1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,390 @@
# Bosch Rexroth ctrlX Data Layer Input Plugin
This plugin gathers data from the [ctrlX Data Layer][ctrlx] a communication
middleware running on Bosch Rexroth's [ctrlX CORE devices][core_devs]. The
platform is used for professional automation applications like industrial
automation, building automation, robotics, IoT Gateways or as classical PLC.
⭐ Telegraf v1.27.0
🏷️ iot, messaging
💻 all
[ctrlx]: https://ctrlx-automation.com
[core_devs]: https://ctrlx-core.com
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample.conf
# A ctrlX Data Layer server sent event input plugin
[[inputs.ctrlx_datalayer]]
## Hostname or IP address of the ctrlX CORE Data Layer server
## example: server = "localhost" # Telegraf is running directly on the device
## server = "192.168.1.1" # Connect to ctrlX CORE remote via IP
## server = "host.example.com" # Connect to ctrlX CORE remote via hostname
## server = "10.0.2.2:8443" # Connect to ctrlX CORE Virtual from development environment
server = "localhost"
## Authentication credentials
username = "boschrexroth"
password = "boschrexroth"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
## Timeout for HTTP requests. (default: "10s")
# timeout = "10s"
## Create a ctrlX Data Layer subscription.
## It is possible to define multiple subscriptions per host. Each subscription can have its own
## sampling properties and a list of nodes to subscribe to.
## All subscriptions share the same credentials.
[[inputs.ctrlx_datalayer.subscription]]
## The name of the measurement. (default: "ctrlx")
measurement = "memory"
## Configure the ctrlX Data Layer nodes which should be subscribed.
## address - node address in ctrlX Data Layer (mandatory)
## name - field name to use in the output (optional, default: base name of address)
## tags - extra node tags to be added to the output metric (optional)
## Note:
## Use either the inline notation or the bracketed notation, not both.
## The tags property is only supported in bracketed notation due to toml parser restrictions
## Examples:
## Inline notation
nodes=[
{name="available", address="framework/metrics/system/memavailable-mb"},
{name="used", address="framework/metrics/system/memused-mb"},
]
## Bracketed notation
# [[inputs.ctrlx_datalayer.subscription.nodes]]
# name ="available"
# address="framework/metrics/system/memavailable-mb"
# ## Define extra tags related to node to be added to the output metric (optional)
# [inputs.ctrlx_datalayer.subscription.nodes.tags]
# node_tag1="node_tag1"
# node_tag2="node_tag2"
# [[inputs.ctrlx_datalayer.subscription.nodes]]
# name ="used"
# address="framework/metrics/system/memused-mb"
## The switch "output_json_string" enables output of the measurement as json.
## That way it can be used in in a subsequent processor plugin, e.g. "Starlark Processor Plugin".
# output_json_string = false
## Define extra tags related to subscription to be added to the output metric (optional)
# [inputs.ctrlx_datalayer.subscription.tags]
# subscription_tag1 = "subscription_tag1"
# subscription_tag2 = "subscription_tag2"
## The interval in which messages shall be sent by the ctrlX Data Layer to this plugin. (default: 1s)
## Higher values reduce load on network by queuing samples on server side and sending as a single TCP packet.
# publish_interval = "1s"
## The interval a "keepalive" message is sent if no change of data occurs. (default: 60s)
## Only used internally to detect broken network connections.
# keep_alive_interval = "60s"
## The interval an "error" message is sent if an error was received from a node. (default: 10s)
## Higher values reduce load on output target and network in case of errors by limiting frequency of error messages.
# error_interval = "10s"
## The interval that defines the fastest rate at which the node values should be sampled and values captured. (default: 1s)
## The sampling frequency should be adjusted to the dynamics of the signal to be sampled.
## Higher sampling frequencies increases load on ctrlX Data Layer.
## The sampling frequency can be higher, than the publish interval. Captured samples are put in a queue and sent in publish interval.
## Note: The minimum sampling interval can be overruled by a global setting in the ctrlX Data Layer configuration ('datalayer/subscriptions/settings').
# sampling_interval = "1s"
## The requested size of the node value queue. (default: 10)
## Relevant if more values are captured than can be sent.
# queue_size = 10
## The behaviour of the queue if it is full. (default: "DiscardOldest")
## Possible values:
## - "DiscardOldest"
## The oldest value gets deleted from the queue when it is full.
## - "DiscardNewest"
## The newest value gets deleted from the queue when it is full.
# queue_behaviour = "DiscardOldest"
## The filter when a new value will be sampled. (default: 0.0)
## Calculation rule: If (abs(lastCapturedValue - newValue) > dead_band_value) capture(newValue).
# dead_band_value = 0.0
## The conditions on which a sample should be captured and thus will be sent as a message. (default: "StatusValue")
## Possible values:
## - "Status"
## Capture the value only, when the state of the node changes from or to error state. Value changes are ignored.
## - "StatusValue"
## Capture when the value changes or the node changes from or to error state.
## See also 'dead_band_value' for what is considered as a value change.
## - "StatusValueTimestamp":
## Capture even if the value is the same, but the timestamp of the value is newer.
## Note: This might lead to high load on the network because every sample will be sent as a message
## even if the value of the node did not change.
# value_change = "StatusValue"
```
## Metrics
All measurements are tagged with the server address of the device and the
corresponding node address as defined in the ctrlX Data Layer.
- measurement name
- tags:
- `source` (ctrlX Data Layer server where the metrics are gathered from)
- `node` (Address of the ctrlX Data Layer node)
- fields:
- `{name}` (for nodes with simple data types)
- `{name}_{index}`(for nodes with array data types)
- `{name}_{jsonflat.key}` (for nodes with object data types)
### Output Format
The switch "output_json_string" determines the format of the output metric.
#### Output default format
With the output default format
```toml
output_json_string=false
```
the output is formatted automatically as follows depending on the data type:
##### Simple data type
The value is passed 'as it is' to a metric with pattern:
```text
{name}={value}
```
Simple data types of ctrlX Data Layer:
```text
bool8,int8,uint8,int16,uint16,int32,uint32,int64,uint64,float,double,string,timestamp
```
##### Array data type
Every value in the array is passed to a metric with pattern:
```text
{name}_{index}={value[index]}
```
example:
```text
myarray=[1,2,3] -> myarray_1=1, myarray_2=2, myarray_3=3
```
Array data types of ctrlX Data Layer:
```text
arbool8,arint8,aruint8,arint16,aruint16,arint32,aruint32,arint64,aruint64,arfloat,ardouble,arstring,artimestamp
```
##### Object data type (JSON)
Every value of the flattened json is passed to a metric with pattern:
```text
{name}_{jsonflat.key}={jsonflat.value}
```
example:
```text
myobj={"a":1,"b":2,"c":{"d": 3}} -> myobj_a=1, myobj_b=2, myobj_c_d=3
```
#### Output JSON format
With the output JSON format
```toml
output_json_string=true
```
the output is formatted as JSON string:
```text
{name}="{value}"
```
examples:
```text
input=true -> output="true"
```
```text
input=[1,2,3] -> output="[1,2,3]"
```
```text
input={"x":4720,"y":9440,"z":{"d": 14160}} -> output="{\"x\":4720,\"y\":9440,\"z\":14160}"
```
The JSON output string can be passed to a processor plugin for transformation
e.g. [Parser Processor Plugin][PARSER.md]
or [Starlark Processor Plugin][STARLARK.md]
[PARSER.md]: ../../processors/parser/README.md
[STARLARK.md]: ../../processors/starlark/README.md
example:
```toml
[[inputs.ctrlx_datalayer.subscription]]
measurement = "osci"
nodes = [
{address="oscilloscope/instances/Osci_PLC/rec-values/allsignals"},
]
output_json_string = true
[[processors.starlark]]
namepass = [
'osci',
]
script = "oscilloscope.star"
```
## Troubleshooting
This plugin was contributed by
[Bosch Rexroth](https://www.boschrexroth.com).
For questions regarding ctrlX AUTOMATION and this plugin feel
free to check out and be part of the
[ctrlX AUTOMATION Community](https://ctrlx-automation.com/community)
to get additional support or leave some ideas and feedback.
Also, join
[InfluxData Community Slack](https://influxdata.com/slack) or
[InfluxData Community Page](https://community.influxdata.com/)
if you have questions or comments for the telegraf engineering teams.
## Example Output
The plugin handles simple, array and object (JSON) data types.
### Example with simple data type
Configuration:
```toml
[[inputs.ctrlx_datalayer.subscription]]
measurement="memory"
[inputs.ctrlx_datalayer.subscription.tags]
sub_tag1="memory_tag1"
sub_tag2="memory_tag2"
[[inputs.ctrlx_datalayer.subscription.nodes]]
name ="available"
address="framework/metrics/system/memavailable-mb"
[inputs.ctrlx_datalayer.subscription.nodes.tags]
node_tag1="memory_available_tag1"
node_tag2="memory_available_tag2"
[[inputs.ctrlx_datalayer.subscription.nodes]]
name ="used"
address="framework/metrics/system/memused-mb"
[inputs.ctrlx_datalayer.subscription.nodes.tags]
node_tag1="memory_used_node_tag1"
node_tag2="memory_used_node_tag2"
```
Source:
```json
"framework/metrics/system/memavailable-mb" : 365.93359375
"framework/metrics/system/memused-mb" : 567.67578125
```
Metrics:
```text
memory,source=192.168.1.1,host=host.example.com,node=framework/metrics/system/memavailable-mb,node_tag1=memory_available_tag1,node_tag2=memory_available_tag2,sub_tag1=memory2_tag1,sub_tag2=memory_tag2 available=365.93359375 1680093310249627400
memory,source=192.168.1.1,host=host.example.com,node=framework/metrics/system/memused-mb,node_tag1=memory_used_node_tag1,node_tag2=memory_used_node_tag2,sub_tag1=memory2_tag1,sub_tag2=memory_tag2 used=567.67578125 1680093310249667600
```
### Example with array data type
Configuration:
```toml
[[inputs.ctrlx_datalayer.subscription]]
measurement="array"
nodes=[
{ name="ar_uint8", address="alldata/dynamic/array-of-uint8"},
{ name="ar_bool8", address="alldata/dynamic/array-of-bool8"},
]
```
Source:
```json
"alldata/dynamic/array-of-bool8" : [true, false, true]
"alldata/dynamic/array-of-uint8" : [0, 255]
```
Metrics:
```text
array,source=192.168.1.1,host=host.example.com,node=alldata/dynamic/array-of-bool8 ar_bool8_0=true,ar_bool8_1=false,ar_bool8_2=true 1680095727347018800
array,source=192.168.1.1,host=host.example.com,node=alldata/dynamic/array-of-uint8 ar_uint8_0=0,ar_uint8_1=255 1680095727347223300
```
### Example with object data type (JSON)
Configuration:
```toml
[[inputs.ctrlx_datalayer.subscription]]
measurement="motion"
nodes=[
{name="linear", address="motion/axs/Axis_1/state/values/actual"},
{name="rotational", address="motion/axs/Axis_2/state/values/actual"},
]
```
Source:
```json
"motion/axs/Axis_1/state/values/actual" : {"actualPos":65.249329860957,"actualVel":5,"actualAcc":0,"actualTorque":0,"distLeft":0,"actualPosUnit":"mm","actualVelUnit":"mm/min","actualAccUnit":"m/s^2","actualTorqueUnit":"Nm","distLeftUnit":"mm"}
"motion/axs/Axis_2/state/values/actual" : {"actualPos":120,"actualVel":0,"actualAcc":0,"actualTorque":0,"distLeft":0,"actualPosUnit":"deg","actualVelUnit":"rpm","actualAccUnit":"rad/s^2","actualTorqueUnit":"Nm","distLeftUnit":"deg"}
```
Metrics:
```text
motion,source=192.168.1.1,host=host.example.com,node=motion/axs/Axis_1/state/values/actual linear_actualVel=5,linear_distLeftUnit="mm",linear_actualAcc=0,linear_distLeft=0,linear_actualPosUnit="mm",linear_actualAccUnit="m/s^2",linear_actualTorqueUnit="Nm",linear_actualPos=65.249329860957,linear_actualVelUnit="mm/min",linear_actualTorque=0 1680258290342523500
motion,source=192.168.1.1,host=host.example.com,node=motion/axs/Axis_2/state/values/actual rotational_distLeft=0,rotational_actualVelUnit="rpm",rotational_actualAccUnit="rad/s^2",rotational_distLeftUnit="deg",rotational_actualPos=120,rotational_actualVel=0,rotational_actualAcc=0,rotational_actualPosUnit="deg",rotational_actualTorqueUnit="Nm",rotational_actualTorque=0 1680258290342538100
```
If `output_json_string` is set in the configuration:
```toml
output_json_string = true
```
then the metrics will be generated like this:
```text
motion,source=192.168.1.1,host=host.example.com,node=motion/axs/Axis_1/state/values/actual linear="{\"actualAcc\":0,\"actualAccUnit\":\"m/s^2\",\"actualPos\":65.249329860957,\"actualPosUnit\":\"mm\",\"actualTorque\":0,\"actualTorqueUnit\":\"Nm\",\"actualVel\":5,\"actualVelUnit\":\"mm/min\",\"distLeft\":0,\"distLeftUnit\":\"mm\"}" 1680258290342523500
motion,source=192.168.1.1,host=host.example.com,node=motion/axs/Axis_2/state/values/actual rotational="{\"actualAcc\":0,\"actualAccUnit\":\"rad/s^2\",\"actualPos\":120,\"actualPosUnit\":\"deg\",\"actualTorque\":0,\"actualTorqueUnit\":\"Nm\",\"actualVel\":0,\"actualVelUnit\":\"rpm\",\"distLeft\":0,\"distLeftUnit\":\"deg\"}" 1680258290342538100
```

View file

@ -0,0 +1,371 @@
//go:generate ../../../tools/readme_config_includer/generator
package ctrlx_datalayer
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/boschrexroth/ctrlx-datalayer-golang/pkg/sseclient"
"github.com/boschrexroth/ctrlx-datalayer-golang/pkg/token"
"github.com/google/uuid"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal/choice"
"github.com/influxdata/telegraf/metric"
common_http "github.com/influxdata/telegraf/plugins/common/http"
"github.com/influxdata/telegraf/plugins/inputs"
parsers_json "github.com/influxdata/telegraf/plugins/parsers/json"
)
// This plugin is based on the official ctrlX CORE API. Documentation can be found in OpenAPI format at:
// https://boschrexroth.github.io/rest-api-description/ctrlx-automation/ctrlx-core/
// Used APIs are:
// * ctrlX CORE - Authorization and Authentication API
// * ctrlX CORE - Data Layer API
//
// All communication between the device and this input plugin is based
// on https REST and HTML5 Server Sent Events (sse).
//go:embed sample.conf
var sampleConfig string
type CtrlXDataLayer struct {
Server string `toml:"server"`
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
Log telegraf.Logger `toml:"-"`
Subscription []subscription
url string
wg sync.WaitGroup
cancel context.CancelFunc
acc telegraf.Accumulator
connection *http.Client
tokenManager token.TokenManager
common_http.HTTPClientConfig
}
func (*CtrlXDataLayer) SampleConfig() string {
return sampleConfig
}
func (c *CtrlXDataLayer) Init() error {
// Check all configured subscriptions for valid settings
for i := range c.Subscription {
sub := &c.Subscription[i]
sub.applyDefaultSettings()
if !choice.Contains(sub.QueueBehaviour, queueBehaviours) {
c.Log.Infof("The right queue behaviour values are %v", queueBehaviours)
return fmt.Errorf("subscription %d: setting 'queue_behaviour' %q is invalid", i, sub.QueueBehaviour)
}
if !choice.Contains(sub.ValueChange, valueChanges) {
c.Log.Infof("The right value change values are %v", valueChanges)
return fmt.Errorf("subscription %d: setting 'value_change' %q is invalid", i, sub.ValueChange)
}
if len(sub.Nodes) == 0 {
c.Log.Warn("A configured subscription has no nodes configured")
}
sub.index = i
}
// Generate valid communication url based on configured server address
u := url.URL{
Scheme: "https",
Host: c.Server,
}
c.url = u.String()
if _, err := url.Parse(c.url); err != nil {
return errors.New("invalid server address")
}
return nil
}
func (c *CtrlXDataLayer) Start(acc telegraf.Accumulator) error {
var ctx context.Context
ctx, c.cancel = context.WithCancel(context.Background())
var err error
c.connection, err = c.HTTPClientConfig.CreateClient(ctx, c.Log)
if err != nil {
return fmt.Errorf("failed to create http client: %w", err)
}
username, err := c.Username.Get()
if err != nil {
return fmt.Errorf("getting username failed: %w", err)
}
password, err := c.Password.Get()
if err != nil {
username.Destroy()
return fmt.Errorf("getting password failed: %w", err)
}
c.tokenManager = token.TokenManager{
Url: c.url,
Username: username.String(),
Password: password.String(),
Connection: c.connection,
}
username.Destroy()
password.Destroy()
c.acc = acc
c.gatherLoop(ctx)
return nil
}
func (*CtrlXDataLayer) Gather(telegraf.Accumulator) error {
// Metrics are sent to the accumulator asynchronously in worker thread. So nothing to do here.
return nil
}
func (c *CtrlXDataLayer) Stop() {
c.cancel()
c.wg.Wait()
if c.connection != nil {
c.connection.CloseIdleConnections()
}
}
// convertTimestamp2UnixTime converts the given Data Layer timestamp of the payload to UnixTime.
func convertTimestamp2UnixTime(t int64) time.Time {
// 1 sec=1000 millisec=1000000 microsec=1000000000 nanosec.
// Convert from FILETIME (100-nanosecond intervals since January 1, 1601 UTC) to
// seconds and nanoseconds since January 1, 1970 UTC.
// Between Jan 1, 1601 and Jan 1, 1970 there are 11644473600 seconds.
return time.Unix(0, (t-116444736000000000)*100)
}
// createSubscription uses the official 'ctrlX Data Layer API' to create the sse subscription.
func (c *CtrlXDataLayer) createSubscription(sub *subscription) (string, error) {
sseURL := c.url + subscriptionPath
id := "telegraf_" + uuid.New().String()
request := sub.createRequest(id)
payload, err := json.Marshal(request)
if err != nil {
return "", fmt.Errorf("failed to create subscription %d payload: %w", sub.index, err)
}
requestBody := bytes.NewBuffer(payload)
req, err := http.NewRequest("POST", sseURL, requestBody)
if err != nil {
return "", fmt.Errorf("failed to create subscription %d request: %w", sub.index, err)
}
req.Header.Add("Authorization", c.tokenManager.Token.String())
resp, err := c.connection.Do(req)
if err != nil {
return "", fmt.Errorf("failed to do request to create sse subscription %d: %w", sub.index, err)
}
resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 201 {
return "", fmt.Errorf("failed to create sse subscription %d, status: %s", sub.index, resp.Status)
}
return sseURL + "/" + id, nil
}
// createSubscriptionAndSseClient creates a sse subscription on the server and
// initializes a sse client to receive sse events from the server.
func (c *CtrlXDataLayer) createSubscriptionAndSseClient(sub *subscription) (*sseclient.SseClient, error) {
t, err := c.tokenManager.RequestAuthToken()
if err != nil {
return nil, err
}
subURL, err := c.createSubscription(sub)
if err != nil {
return nil, err
}
client := sseclient.NewSseClient(subURL, t.String(), c.InsecureSkipVerify)
return client, nil
}
// addMetric writes sse metric into accumulator.
func (c *CtrlXDataLayer) addMetric(se *sseclient.SseEvent, sub *subscription) {
switch se.Event {
case "update":
// Received an updated value, that we translate into a metric
var d sseEventData
if err := json.Unmarshal([]byte(se.Data), &d); err != nil {
c.acc.AddError(fmt.Errorf("received malformed data from 'update' event: %w", err))
return
}
m, err := c.createMetric(&d, sub)
if err != nil {
c.acc.AddError(fmt.Errorf("failed to create metrics: %w", err))
return
}
c.acc.AddMetric(m)
case "error":
// Received an error event, that we report to the accumulator
var e sseEventError
if err := json.Unmarshal([]byte(se.Data), &e); err != nil {
c.acc.AddError(fmt.Errorf("received malformed data from 'error' event: %w", err))
return
}
c.acc.AddError(fmt.Errorf("received 'error' event for node: %q", e.Instance))
case "keepalive":
// Keepalive events are ignored for the moment
c.Log.Debug("Received keepalive event")
default:
// Received a yet unsupported event type
c.Log.Debugf("Received unsupported event: %q", se.Event)
}
}
// createMetric - create metric depending on flag 'output_json' and data type
func (c *CtrlXDataLayer) createMetric(em *sseEventData, sub *subscription) (telegraf.Metric, error) {
t := convertTimestamp2UnixTime(em.Timestamp)
node := sub.node(em.Node)
if node == nil {
return nil, errors.New("node not found")
}
// default tags
tags := map[string]string{
"node": em.Node,
"source": c.Server,
}
// add tags of subscription if user has defined
for key, value := range sub.Tags {
tags[key] = value
}
// add tags of node if user has defined
for key, value := range node.Tags {
tags[key] = value
}
// set measurement of subscription
measurement := sub.Measurement
// get field key from node properties
fieldKey := node.fieldKey()
if fieldKey == "" {
return nil, errors.New("field key not valid")
}
if sub.OutputJSONString {
b, err := json.Marshal(em.Value)
if err != nil {
return nil, err
}
fields := map[string]interface{}{fieldKey: string(b)}
m := metric.New(measurement, tags, fields, t)
return m, nil
}
switch em.Type {
case "object":
flattener := parsers_json.JSONFlattener{}
err := flattener.FullFlattenJSON(fieldKey, em.Value, true, true)
if err != nil {
return nil, err
}
m := metric.New(measurement, tags, flattener.Fields, t)
return m, nil
case "arbool8",
"arint8", "aruint8",
"arint16", "aruint16",
"arint32", "aruint32",
"arint64", "aruint64",
"arfloat", "ardouble",
"arstring",
"artimestamp":
fields := make(map[string]interface{})
values := em.Value.([]interface{})
for i := 0; i < len(values); i++ {
index := strconv.Itoa(i)
key := fieldKey + "_" + index
fields[key] = values[i]
}
m := metric.New(measurement, tags, fields, t)
return m, nil
case "bool8",
"int8", "uint8",
"int16", "uint16",
"int32", "uint32",
"int64", "uint64",
"float", "double",
"string",
"timestamp":
fields := map[string]interface{}{fieldKey: em.Value}
m := metric.New(measurement, tags, fields, t)
return m, nil
}
return nil, fmt.Errorf("unsupported value type: %s", em.Type)
}
// gatherLoop creates sse subscriptions on the Data Layer and requests the sse data
// the connection will be restablished if the sse subscription is broken.
func (c *CtrlXDataLayer) gatherLoop(ctx context.Context) {
for _, sub := range c.Subscription {
c.wg.Add(1)
go func(sub subscription) {
defer c.wg.Done()
for {
select {
case <-ctx.Done():
c.Log.Debugf("Gather loop for subscription %d stopped", sub.index)
return
default:
client, err := c.createSubscriptionAndSseClient(&sub)
if err != nil {
c.Log.Errorf("Creating sse client to subscription %d: %v", sub.index, err)
time.Sleep(time.Duration(defaultReconnectInterval))
continue
}
c.Log.Debugf("Created sse client to subscription %d", sub.index)
// Establish connection and handle events in a callback function.
err = client.Subscribe(ctx, func(event string, data string) {
c.addMetric(&sseclient.SseEvent{
Event: event,
Data: data,
}, &sub)
})
if errors.Is(err, context.Canceled) {
// Subscription cancelled
c.Log.Debugf("Requesting data of subscription %d cancelled", sub.index)
return
}
c.Log.Errorf("Requesting data of subscription %d failed: %v", sub.index, err)
}
}
}(sub)
}
}
// init registers the plugin in telegraf.
func init() {
inputs.Add("ctrlx_datalayer", func() telegraf.Input {
return &CtrlXDataLayer{}
})
}

View file

@ -0,0 +1,19 @@
package ctrlx_datalayer
// Once a subscription is created, the server will send event notifications to this plugin.
// This file contains the different event types and the included event payload.
// sseEventData represents the json structure send by the ctrlX CORE
// server on an "update" event.
type sseEventData struct {
Node string `json:"node"`
Timestamp int64 `json:"timestamp"`
Type string `json:"type"`
Value interface{} `json:"value"`
}
// sseEventError represents the json structure send by the ctrlX CORE
// server on an "error" event.
type sseEventError struct {
Instance string `json:"instance"`
}

View file

@ -0,0 +1,188 @@
package ctrlx_datalayer
import (
"strings"
"time"
"github.com/influxdata/telegraf/config"
)
// A subscription can be used to watch multiple ctrlX Data Layer nodes for changes.
// Additional configuration settings can be given to tune the sampling and monitoring behaviour of the nodes.
// All nodes in a subscription share the same configuration.
// The plugin is able to create and manage multiple subscriptions.
// The allowed values of the subscription property 'QueueBehaviour'
var queueBehaviours = []string{"DiscardOldest", "DiscardNewest"}
// The allowed values of the subscription property 'ValueChange'
var valueChanges = []string{"Status", "StatusValue", "StatusValueTimestamp"}
// The default subscription settings
const (
defaultKeepaliveInterval = config.Duration(60 * time.Second)
defaultErrorInterval = config.Duration(10 * time.Second)
defaultReconnectInterval = config.Duration(10 * time.Second)
defaultPublishInterval = config.Duration(1 * time.Second)
defaultSamplingInterval = config.Duration(1 * time.Second)
defaultQueueSize = 10
defaultQueueBehaviour = "DiscardOldest"
defaultValueChange = "StatusValue"
defaultMeasurementName = "ctrlx"
subscriptionPath = "/automation/api/v2/events"
)
// node contains all properties of a node configuration
type node struct {
Name string `toml:"name"`
Address string `toml:"address"`
Tags map[string]string `toml:"tags"`
}
// subscription contains all properties of a subscription configuration
type subscription struct {
index int
Nodes []node `toml:"nodes"`
Tags map[string]string `toml:"tags"`
Measurement string `toml:"measurement"`
PublishInterval config.Duration `toml:"publish_interval"`
KeepaliveInterval config.Duration `toml:"keep_alive_interval"`
ErrorInterval config.Duration `toml:"error_interval"`
SamplingInterval config.Duration `toml:"sampling_interval"`
QueueSize uint `toml:"queue_size"`
QueueBehaviour string `toml:"queue_behaviour"`
DeadBandValue float64 `toml:"dead_band_value"`
ValueChange string `toml:"value_change"`
OutputJSONString bool `toml:"output_json_string"`
}
// rule can be used to override default rule settings.
type rule struct {
RuleType string `json:"rule_type"`
Rule interface{} `json:"rule"`
}
// sampling can be used to override default sampling settings.
type sampling struct {
SamplingInterval uint64 `json:"samplingInterval"`
}
// queueing can be used to override default queuing settings.
type queueing struct {
QueueSize uint `json:"queueSize"`
Behaviour string `json:"behaviour"`
}
// dataChangeFilter can be used to override default data change filter settings.
type dataChangeFilter struct {
DeadBandValue float64 `json:"deadBandValue"`
}
// changeEvents can be used to override default change events settings.
type changeEvents struct {
ValueChange string `json:"valueChange"`
BrowselistChange bool `json:"browselistChange"`
MetadataChange bool `json:"metadataChange"`
}
// subscriptionProperties can be used to override default subscription settings.
type subscriptionProperties struct {
KeepaliveInterval int64 `json:"keepaliveInterval"`
Rules []rule `json:"rules"`
ID string `json:"id"`
PublishInterval int64 `json:"publishInterval"`
ErrorInterval int64 `json:"errorInterval"`
}
// subscriptionRequest can be used to create a sse subscription at the ctrlX Data Layer.
type subscriptionRequest struct {
Properties subscriptionProperties `json:"properties"`
Nodes []string `json:"nodes"`
}
// applyDefaultSettings applies the default settings if they are not configured in the config file.
func (s *subscription) applyDefaultSettings() {
if s.Measurement == "" {
s.Measurement = defaultMeasurementName
}
if s.PublishInterval == 0 {
s.PublishInterval = defaultPublishInterval
}
if s.KeepaliveInterval == 0 {
s.KeepaliveInterval = defaultKeepaliveInterval
}
if s.ErrorInterval == 0 {
s.ErrorInterval = defaultErrorInterval
}
if s.SamplingInterval == 0 {
s.SamplingInterval = defaultSamplingInterval
}
if s.QueueSize == 0 {
s.QueueSize = defaultQueueSize
}
if s.QueueBehaviour == "" {
s.QueueBehaviour = defaultQueueBehaviour
}
if s.ValueChange == "" {
s.ValueChange = defaultValueChange
}
}
// createRequestBody builds the request body for the sse subscription, based on the subscription configuration.
// The request body can be send to the server to create a new subscription.
func (s *subscription) createRequest(id string) subscriptionRequest {
pl := subscriptionRequest{
Properties: subscriptionProperties{
Rules: []rule{
{"Sampling", sampling{uint64(time.Duration(s.SamplingInterval).Microseconds())}},
{"Queueing", queueing{s.QueueSize, s.QueueBehaviour}},
{"DataChangeFilter", dataChangeFilter{s.DeadBandValue}},
{"ChangeEvents", changeEvents{s.ValueChange, false, false}},
},
ID: id,
KeepaliveInterval: time.Duration(s.KeepaliveInterval).Milliseconds(),
PublishInterval: time.Duration(s.PublishInterval).Milliseconds(),
ErrorInterval: time.Duration(s.ErrorInterval).Milliseconds(),
},
Nodes: s.addressList(),
}
return pl
}
// addressList lists all configured node addresses
func (s *subscription) addressList() []string {
addressList := make([]string, 0)
for _, node := range s.Nodes {
addressList = append(addressList, node.Address)
}
return addressList
}
// node finds the node according the node address
func (s *subscription) node(address string) *node {
for _, node := range s.Nodes {
if address == node.Address {
return &node
}
}
return nil
}
// fieldKey determines the field key out of node name or address
func (n *node) fieldKey() string {
if n.Name != "" {
// return user defined node name as field key
return n.Name
}
// fallback: field key is extracted from mandatory node address
i := strings.LastIndex(n.Address, "/")
if i > 0 {
// return last part of node address as field key
return n.Address[i+1:]
}
// return full node address as field key
return n.Address
}

View file

@ -0,0 +1,249 @@
package ctrlx_datalayer
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/config"
)
func TestSubscription_createRequest(t *testing.T) {
tests := []struct {
name string
subscription subscription
id string
wantBody subscriptionRequest
wantErr bool
}{
{
name: "Should_Return_Expected_Request",
subscription: subscription{
Nodes: []node{
{
Name: "node1",
Address: "path/to/node1",
Tags: map[string]string{},
},
{
Name: "node2",
Address: "path/to/node2",
Tags: map[string]string{},
},
},
Tags: map[string]string{},
Measurement: "",
PublishInterval: config.Duration(2 * time.Second),
KeepaliveInterval: config.Duration(10 * time.Second),
ErrorInterval: config.Duration(20 * time.Second),
SamplingInterval: config.Duration(100 * time.Millisecond),
QueueSize: 100,
QueueBehaviour: "DiscardNewest",
DeadBandValue: 1.12345,
ValueChange: "StatusValueTimestamp",
OutputJSONString: true,
},
id: "sub_id",
wantBody: subscriptionRequest{
Properties: subscriptionProperties{
KeepaliveInterval: 10000,
Rules: []rule{
{
"Sampling",
sampling{
SamplingInterval: 100000,
},
},
{
"Queueing",
queueing{
QueueSize: 100,
Behaviour: "DiscardNewest",
},
},
{
"DataChangeFilter",
dataChangeFilter{
DeadBandValue: 1.12345,
},
},
{
"ChangeEvents",
changeEvents{
ValueChange: "StatusValueTimestamp",
},
},
},
ID: "sub_id",
PublishInterval: 2000,
ErrorInterval: 20000,
},
Nodes: []string{
"path/to/node1",
"path/to/node2",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.subscription.createRequest(tt.id)
require.Equal(t, tt.wantBody, got)
})
}
}
func TestSubscription_node(t *testing.T) {
tests := []struct {
name string
nodes []node
address string
want *node
}{
{
name: "Should_Return_Node_Of_Given_Address",
nodes: []node{
{
Name: "node1",
Address: "path/to/node1",
Tags: map[string]string{},
},
{
Name: "node2",
Address: "path/to/node2",
Tags: map[string]string{},
},
{
Name: "",
Address: "path/to/node3",
Tags: map[string]string{},
},
},
address: "path/to/node3",
want: &node{
Name: "",
Address: "path/to/node3",
Tags: map[string]string{},
},
},
{
name: "Should_Return_Nil_If_Node_With_Given_Address_Not_Found",
nodes: []node{
{
Name: "Node1",
Address: "path/to/node1",
Tags: map[string]string{},
},
{
Name: "Node2",
Address: "path/to/node2",
Tags: map[string]string{},
},
{
Name: "",
Address: "path/to/node3",
Tags: map[string]string{},
},
},
address: "path/to/node4",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &subscription{
Nodes: tt.nodes,
}
require.Equal(t, tt.want, s.node(tt.address))
})
}
}
func TestSubscription_addressList(t *testing.T) {
tests := []struct {
name string
nodes []node
want []string
}{
{
name: "Should_Return_AddressArray_Of_All_Nodes",
nodes: []node{
{
Address: "framework/metrics/system/memused-mb",
},
{
Address: "framework/metrics/system/memavailable-mb",
},
{
Address: "root",
},
{
Address: "",
},
},
want: []string{
"framework/metrics/system/memused-mb",
"framework/metrics/system/memavailable-mb",
"root",
"",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &subscription{
Nodes: tt.nodes,
}
require.Equal(t, tt.want, s.addressList())
})
}
}
func TestNode_fieldKey(t *testing.T) {
tests := []struct {
name string
node node
want string
}{
{
name: "Should_Return_Name_When_Name_Is_Not_Empty",
node: node{
Name: "used",
Address: "framework/metrics/system/memused-mb",
},
want: "used",
},
{
name: "Should_Return_Address_Base_When_Name_Is_Empty_And_Address_Contains_Full_Path",
node: node{
Name: "",
Address: "framework/metrics/system/memused-mb",
},
want: "memused-mb",
},
{
name: "Should_Return_Address_Base_Root_When_Name_Is_Empty_And_Address_Contains_Root_Path",
node: node{
Name: "",
Address: "root",
},
want: "root",
},
{
name: "Should_Return_Empty_When_Name_and_Address_Are_Empty",
node: node{
Name: "",
Address: "",
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, tt.node.fieldKey())
})
}
}

View file

@ -0,0 +1,293 @@
package ctrlx_datalayer
import (
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/boschrexroth/ctrlx-datalayer-golang/pkg/token"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/config"
common_http "github.com/influxdata/telegraf/plugins/common/http"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/testutil"
)
const path = "/automation/api/v2/events"
var multiEntries = false
var mux sync.Mutex
func setMultiEntries(m bool) {
mux.Lock()
defer mux.Unlock()
multiEntries = m
}
func getMultiEntries() bool {
mux.Lock()
defer mux.Unlock()
return multiEntries
}
func TestCtrlXCreateSubscriptionBasic(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
if _, err := w.Write([]byte("201 created")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
subs := make([]subscription, 0)
subs = append(subs, subscription{
index: 0,
Nodes: []node{
{Name: "counter", Address: "plc/app/Application/sym/PLC_PRG/counter"},
{Name: "counterReverse", Address: "plc/app/Application/sym/PLC_PRG/counterReverse"},
},
},
)
s := &CtrlXDataLayer{
connection: &http.Client{},
url: server.URL,
Username: config.NewSecret([]byte("user")),
Password: config.NewSecret([]byte("password")),
tokenManager: token.TokenManager{Url: server.URL, Username: "user", Password: "password", Connection: &http.Client{}},
Subscription: subs,
Log: testutil.Logger{},
}
subID, err := s.createSubscription(&subs[0])
require.NoError(t, err)
require.NotEmpty(t, subID)
}
func TestCtrlXCreateSubscriptionDriven(t *testing.T) {
var tests = []struct {
res string
status int
wantError bool
}{
{res: "{\"status\":200}", status: 200, wantError: false},
{res: "{\"status\":401}", status: 401, wantError: true},
}
for _, test := range tests {
t.Run(test.res, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(test.status)
if _, err := w.Write([]byte(test.res)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
subs := make([]subscription, 0)
subs = append(subs, subscription{
Nodes: []node{
{Name: "counter", Address: "plc/app/Application/sym/PLC_PRG/counter"},
{Name: "counterReverse", Address: "plc/app/Application/sym/PLC_PRG/counterReverse"},
},
},
)
s := &CtrlXDataLayer{
connection: &http.Client{},
url: server.URL,
Username: config.NewSecret([]byte("user")),
Password: config.NewSecret([]byte("password")),
Subscription: subs,
tokenManager: token.TokenManager{Url: server.URL, Username: "user", Password: "password", Connection: &http.Client{}},
Log: testutil.Logger{},
}
subID, err := s.createSubscription(&subs[0])
if test.wantError {
require.EqualError(t, err, "failed to create sse subscription 0, status: 401 Unauthorized")
require.Empty(t, subID)
} else {
require.NoError(t, err)
require.NotEmpty(t, subID)
}
})
}
}
func newServer(t *testing.T) *httptest.Server {
mux := http.NewServeMux()
// Handle request to fetch token
mux.HandleFunc("/identity-manager/api/v2/auth/token", func(w http.ResponseWriter, _ *http.Request) {
if _, err := w.Write([]byte("{\"access_token\": \"eyJhbGciOiJIU.xxx.xxx\", \"token_type\":\"Bearer\"}")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
})
// Handle request to validate token
mux.HandleFunc("/identity-manager/api/v2/auth/token/validity", func(w http.ResponseWriter, _ *http.Request) {
if _, err := w.Write([]byte("{\"valid\": \"true\"}")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
})
// Handle request to create subscription
mux.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
if _, err := w.Write([]byte("201 created")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
})
// Handle request to fetch sse data
mux.HandleFunc(path+"/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("event: update\n")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
if _, err := w.Write([]byte("id: 12345\n")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
if getMultiEntries() {
data := "data: {\n"
if _, err := w.Write([]byte(data)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
data = "data: \"node\":\"plc/app/Application/sym/PLC_PRG/counter\", \"timestamp\":132669450604571037,\"type\":\"double\",\"value\":44.0\n"
if _, err := w.Write([]byte(data)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
data = "data: }\n"
if _, err := w.Write([]byte(data)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
} else {
data := "data: {\"node\":\"plc/app/Application/sym/PLC_PRG/counter\", \"timestamp\":132669450604571037,\"type\":\"double\",\"value\":43.0}\n"
if _, err := w.Write([]byte(data)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}
if _, err := w.Write([]byte("\n")); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}
})
return httptest.NewServer(mux)
}
func cleanup(server *httptest.Server) {
server.CloseClientConnections()
server.Close()
}
func initRunner(t *testing.T) (*CtrlXDataLayer, *httptest.Server) {
server := newServer(t)
subs := make([]subscription, 0)
subs = append(subs, subscription{
Measurement: "ctrlx",
Nodes: []node{
{Name: "counter", Address: "plc/app/Application/sym/PLC_PRG/counter"},
},
},
)
s := &CtrlXDataLayer{
connection: &http.Client{},
url: server.URL,
Username: config.NewSecret([]byte("user")),
Password: config.NewSecret([]byte("password")),
HTTPClientConfig: common_http.HTTPClientConfig{
ClientConfig: tls.ClientConfig{
InsecureSkipVerify: true,
},
},
Subscription: subs,
tokenManager: token.TokenManager{Url: server.URL, Username: "user", Password: "password", Connection: &http.Client{}},
Log: testutil.Logger{},
}
return s, server
}
func TestCtrlXMetricsField(t *testing.T) {
const measurement = "ctrlx"
const fieldName = "counter"
s, server := initRunner(t)
defer cleanup(server)
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(s.Start))
require.Eventually(t, func() bool {
if v, found := acc.FloatField(measurement, fieldName); found {
require.InDelta(t, 43.0, v, testutil.DefaultDelta)
return true
}
return false
}, time.Second*10, time.Second)
}
func TestCtrlXMetricsMulti(t *testing.T) {
const measurement = "ctrlx"
const fieldName = "counter"
setMultiEntries(true)
s, server := initRunner(t)
defer cleanup(server)
var acc testutil.Accumulator
require.NoError(t, acc.GatherError(s.Start))
require.Eventually(t, func() bool {
if v, found := acc.FloatField(measurement, fieldName); found {
require.InDelta(t, 44.0, v, testutil.DefaultDelta)
return true
}
return false
}, time.Second*10, time.Second)
setMultiEntries(false)
}
func TestCtrlXCreateSseClient(t *testing.T) {
sub := subscription{
Measurement: "ctrlx",
Nodes: []node{
{Name: "counter", Address: "plc/app/Application/sym/PLC_PRG/counter"},
{Name: "counterReverse", Address: "plc/app/Application/sym/PLC_PRG/counterReverse"},
},
}
s, server := initRunner(t)
defer cleanup(server)
client, err := s.createSubscriptionAndSseClient(&sub)
require.NoError(t, err)
require.NotEmpty(t, client)
}
func TestConvertTimestamp2UnixTime(t *testing.T) {
expected := time.Date(2022, 02, 14, 14, 22, 38, 333552400, time.UTC)
actual := convertTimestamp2UnixTime(132893221583335524)
require.EqualValues(t, expected.UnixNano(), actual.UnixNano())
}

View file

@ -0,0 +1,110 @@
# A ctrlX Data Layer server sent event input plugin
[[inputs.ctrlx_datalayer]]
## Hostname or IP address of the ctrlX CORE Data Layer server
## example: server = "localhost" # Telegraf is running directly on the device
## server = "192.168.1.1" # Connect to ctrlX CORE remote via IP
## server = "host.example.com" # Connect to ctrlX CORE remote via hostname
## server = "10.0.2.2:8443" # Connect to ctrlX CORE Virtual from development environment
server = "localhost"
## Authentication credentials
username = "boschrexroth"
password = "boschrexroth"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
## Timeout for HTTP requests. (default: "10s")
# timeout = "10s"
## Create a ctrlX Data Layer subscription.
## It is possible to define multiple subscriptions per host. Each subscription can have its own
## sampling properties and a list of nodes to subscribe to.
## All subscriptions share the same credentials.
[[inputs.ctrlx_datalayer.subscription]]
## The name of the measurement. (default: "ctrlx")
measurement = "memory"
## Configure the ctrlX Data Layer nodes which should be subscribed.
## address - node address in ctrlX Data Layer (mandatory)
## name - field name to use in the output (optional, default: base name of address)
## tags - extra node tags to be added to the output metric (optional)
## Note:
## Use either the inline notation or the bracketed notation, not both.
## The tags property is only supported in bracketed notation due to toml parser restrictions
## Examples:
## Inline notation
nodes=[
{name="available", address="framework/metrics/system/memavailable-mb"},
{name="used", address="framework/metrics/system/memused-mb"},
]
## Bracketed notation
# [[inputs.ctrlx_datalayer.subscription.nodes]]
# name ="available"
# address="framework/metrics/system/memavailable-mb"
# ## Define extra tags related to node to be added to the output metric (optional)
# [inputs.ctrlx_datalayer.subscription.nodes.tags]
# node_tag1="node_tag1"
# node_tag2="node_tag2"
# [[inputs.ctrlx_datalayer.subscription.nodes]]
# name ="used"
# address="framework/metrics/system/memused-mb"
## The switch "output_json_string" enables output of the measurement as json.
## That way it can be used in in a subsequent processor plugin, e.g. "Starlark Processor Plugin".
# output_json_string = false
## Define extra tags related to subscription to be added to the output metric (optional)
# [inputs.ctrlx_datalayer.subscription.tags]
# subscription_tag1 = "subscription_tag1"
# subscription_tag2 = "subscription_tag2"
## The interval in which messages shall be sent by the ctrlX Data Layer to this plugin. (default: 1s)
## Higher values reduce load on network by queuing samples on server side and sending as a single TCP packet.
# publish_interval = "1s"
## The interval a "keepalive" message is sent if no change of data occurs. (default: 60s)
## Only used internally to detect broken network connections.
# keep_alive_interval = "60s"
## The interval an "error" message is sent if an error was received from a node. (default: 10s)
## Higher values reduce load on output target and network in case of errors by limiting frequency of error messages.
# error_interval = "10s"
## The interval that defines the fastest rate at which the node values should be sampled and values captured. (default: 1s)
## The sampling frequency should be adjusted to the dynamics of the signal to be sampled.
## Higher sampling frequencies increases load on ctrlX Data Layer.
## The sampling frequency can be higher, than the publish interval. Captured samples are put in a queue and sent in publish interval.
## Note: The minimum sampling interval can be overruled by a global setting in the ctrlX Data Layer configuration ('datalayer/subscriptions/settings').
# sampling_interval = "1s"
## The requested size of the node value queue. (default: 10)
## Relevant if more values are captured than can be sent.
# queue_size = 10
## The behaviour of the queue if it is full. (default: "DiscardOldest")
## Possible values:
## - "DiscardOldest"
## The oldest value gets deleted from the queue when it is full.
## - "DiscardNewest"
## The newest value gets deleted from the queue when it is full.
# queue_behaviour = "DiscardOldest"
## The filter when a new value will be sampled. (default: 0.0)
## Calculation rule: If (abs(lastCapturedValue - newValue) > dead_band_value) capture(newValue).
# dead_band_value = 0.0
## The conditions on which a sample should be captured and thus will be sent as a message. (default: "StatusValue")
## Possible values:
## - "Status"
## Capture the value only, when the state of the node changes from or to error state. Value changes are ignored.
## - "StatusValue"
## Capture when the value changes or the node changes from or to error state.
## See also 'dead_band_value' for what is considered as a value change.
## - "StatusValueTimestamp":
## Capture even if the value is the same, but the timestamp of the value is newer.
## Note: This might lead to high load on the network because every sample will be sent as a message
## even if the value of the node did not change.
# value_change = "StatusValue"