Add node groups to opcua input plugin (#8389)
This commit is contained in:
parent
0ccb134ae4
commit
498a6da75f
|
|
@ -9,8 +9,8 @@ Plugin minimum tested version: 1.16
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[inputs.opcua]]
|
[[inputs.opcua]]
|
||||||
## Device name
|
## Metric name
|
||||||
# name = "localhost"
|
# name = "opcua"
|
||||||
#
|
#
|
||||||
## OPC UA Endpoint URL
|
## OPC UA Endpoint URL
|
||||||
# endpoint = "opc.tcp://localhost:4840"
|
# endpoint = "opc.tcp://localhost:4840"
|
||||||
|
|
@ -47,34 +47,97 @@ Plugin minimum tested version: 1.16
|
||||||
# password = ""
|
# password = ""
|
||||||
#
|
#
|
||||||
## Node ID configuration
|
## Node ID configuration
|
||||||
## name - the variable name
|
## name - field name to use in the output
|
||||||
## namespace - integer value 0 thru 3
|
## namespace - OPC UA namespace of the node (integer value 0 thru 3)
|
||||||
## identifier_type - s=string, i=numeric, g=guid, b=opaque
|
## identifier_type - OPC UA ID type (s=string, i=numeric, g=guid, b=opaque)
|
||||||
## identifier - tag as shown in opcua browser
|
## identifier - OPC UA ID (tag as shown in opcua browser)
|
||||||
## data_type - boolean, byte, short, int, uint, uint16, int16,
|
## tags - extra tags to be added to the output metric (optional)
|
||||||
## uint32, int32, float, double, string, datetime, number
|
|
||||||
## Example:
|
## Example:
|
||||||
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"}
|
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", tags=[["tag1","value1"],["tag2","value2]]}
|
||||||
|
# nodes = [
|
||||||
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
|
#]
|
||||||
|
#
|
||||||
|
## Node Group
|
||||||
|
## Sets defaults for OPC UA namespace and ID type so they aren't required in
|
||||||
|
## every node. A group can also have a metric name that overrides the main
|
||||||
|
## plugin metric name.
|
||||||
|
##
|
||||||
|
## Multiple node groups are allowed
|
||||||
|
#[[inputs.opcua.group]]
|
||||||
|
## Group Metric name. Overrides the top level name. If unset, the
|
||||||
|
## top level name is used.
|
||||||
|
# name =
|
||||||
|
#
|
||||||
|
## Group default namespace. If a node in the group doesn't set its
|
||||||
|
## namespace, this is used.
|
||||||
|
# namespace =
|
||||||
|
#
|
||||||
|
## Group default identifier type. If a node in the group doesn't set its
|
||||||
|
## namespace, this is used.
|
||||||
|
# identifier_type =
|
||||||
|
#
|
||||||
|
## Node ID Configuration. Array of nodes with the same settings as above.
|
||||||
|
# nodes = [
|
||||||
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
|
#]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node Configuration
|
||||||
|
An OPC UA node ID may resemble: "n=3;s=Temperature". In this example:
|
||||||
|
- n=3 is indicating the `namespace` is 3
|
||||||
|
- s=Temperature is indicting that the `identifier_type` is a string and `identifier` value is 'Temperature'
|
||||||
|
- This example temperature node has a value of 79.0
|
||||||
|
To gather data from this node enter the following line into the 'nodes' property above:
|
||||||
|
```
|
||||||
|
{field_name="temp", namespace="3", identifier_type="s", identifier="Temperature"},
|
||||||
|
```
|
||||||
|
|
||||||
|
This node configuration produces a metric like this:
|
||||||
|
```
|
||||||
|
opcua,id=n\=3;s\=Temperature temp=79.0,quality="OK (0x0)" 1597820490000000000
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Group Configuration
|
||||||
|
Groups can set default values for the namespace, identifier type, and
|
||||||
|
tags settings. The default values apply to all the nodes in the
|
||||||
|
group. If a default is set, a node may omit the setting altogether.
|
||||||
|
This simplifies node configuration, especially when many nodes share
|
||||||
|
the same namespace or identifier type.
|
||||||
|
|
||||||
|
The output metric will include tags set in the group and the node. If
|
||||||
|
a tag with the same name is set in both places, the tag value from the
|
||||||
|
node is used.
|
||||||
|
|
||||||
|
This example group configuration has two groups with two nodes each:
|
||||||
|
```
|
||||||
|
[[inputs.opcua.group]]
|
||||||
|
name="group1_metric_name"
|
||||||
|
namespace="3"
|
||||||
|
identifier_type="i"
|
||||||
|
tags=[["group1_tag", "val1"]]
|
||||||
nodes = [
|
nodes = [
|
||||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
{name="name", identifier="1001", tags=[["node1_tag", "val2"]]},
|
||||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
{name="name", identifier="1002", tags=[["node1_tag", "val3"]]},
|
||||||
|
]
|
||||||
|
[[inputs.opcua.group]]
|
||||||
|
name="group2_metric_name"
|
||||||
|
namespace="3"
|
||||||
|
identifier_type="i"
|
||||||
|
tags=[["group2_tag", "val3"]]
|
||||||
|
nodes = [
|
||||||
|
{name="saw", identifier="1003", tags=[["node2_tag", "val4"]]},
|
||||||
|
{name="sin", identifier="1004"},
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example Node Configuration
|
It produces metrics like these:
|
||||||
An OPC UA node ID may resemble: "n=3,s=Temperature". In this example:
|
|
||||||
- n=3 is indicating the `namespace` is 3
|
|
||||||
- s=Temperature is indicting that the `identifier_type` is a string and `identifier` value is 'Temperature'
|
|
||||||
- This example temperature node has a value of 79.0, which makes the `data_type` a 'float'.
|
|
||||||
To gather data from this node enter the following line into the 'nodes' property above:
|
|
||||||
```
|
```
|
||||||
{name="LabelName", namespace="3", identifier_type="s", identifier="Temperature", data_type="float", description="Description of node"},
|
group1_metric_name,group1_tag=val1,id=ns\=3;i\=1001,node1_tag=val2 name=0,Quality="OK (0x0)" 1606893246000000000
|
||||||
```
|
group1_metric_name,group1_tag=val1,id=ns\=3;i\=1002,node1_tag=val3 name=-1.389117,Quality="OK (0x0)" 1606893246000000000
|
||||||
|
group2_metric_name,group2_tag=val3,id=ns\=3;i\=1003,node2_tag=val4 Quality="OK (0x0)",saw=-1.6 1606893246000000000
|
||||||
|
group2_metric_name,group2_tag=val3,id=ns\=3;i\=1004 sin=1.902113,Quality="OK (0x0)" 1606893246000000000
|
||||||
### Example Output
|
|
||||||
|
|
||||||
```
|
|
||||||
opcua,host=3c70aee0901e,name=Random,type=double Random=0.018158170305814902 1597820490000000000
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -13,11 +14,12 @@ import (
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
"github.com/influxdata/telegraf/plugins/inputs"
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
|
"github.com/influxdata/telegraf/selfstat"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpcUA type
|
// OpcUA type
|
||||||
type OpcUA struct {
|
type OpcUA struct {
|
||||||
Name string `toml:"name"`
|
MetricName string `toml:"name"`
|
||||||
Endpoint string `toml:"endpoint"`
|
Endpoint string `toml:"endpoint"`
|
||||||
SecurityPolicy string `toml:"security_policy"`
|
SecurityPolicy string `toml:"security_policy"`
|
||||||
SecurityMode string `toml:"security_mode"`
|
SecurityMode string `toml:"security_mode"`
|
||||||
|
|
@ -28,18 +30,18 @@ type OpcUA struct {
|
||||||
AuthMethod string `toml:"auth_method"`
|
AuthMethod string `toml:"auth_method"`
|
||||||
ConnectTimeout config.Duration `toml:"connect_timeout"`
|
ConnectTimeout config.Duration `toml:"connect_timeout"`
|
||||||
RequestTimeout config.Duration `toml:"request_timeout"`
|
RequestTimeout config.Duration `toml:"request_timeout"`
|
||||||
NodeList []OPCTag `toml:"nodes"`
|
RootNodes []NodeSettings `toml:"nodes"`
|
||||||
|
Groups []GroupSettings `toml:"group"`
|
||||||
|
|
||||||
Nodes []string `toml:"-"`
|
nodes []Node
|
||||||
NodeData []OPCData `toml:"-"`
|
nodeData []OPCData
|
||||||
NodeIDs []*ua.NodeID `toml:"-"`
|
nodeIDs []*ua.NodeID
|
||||||
NodeIDerror []error `toml:"-"`
|
nodeIDerror []error
|
||||||
state ConnectionState
|
state ConnectionState
|
||||||
|
|
||||||
// status
|
// status
|
||||||
ReadSuccess int `toml:"-"`
|
ReadSuccess selfstat.Stat `toml:"-"`
|
||||||
ReadError int `toml:"-"`
|
ReadError selfstat.Stat `toml:"-"`
|
||||||
NumberOfTags int `toml:"-"`
|
|
||||||
|
|
||||||
// internal values
|
// internal values
|
||||||
client *opcua.Client
|
client *opcua.Client
|
||||||
|
|
@ -48,13 +50,29 @@ type OpcUA struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPCTag type
|
// OPCTag type
|
||||||
type OPCTag struct {
|
type NodeSettings struct {
|
||||||
Name string `toml:"name"`
|
FieldName string `toml:"name"`
|
||||||
Namespace string `toml:"namespace"`
|
Namespace string `toml:"namespace"`
|
||||||
IdentifierType string `toml:"identifier_type"`
|
IdentifierType string `toml:"identifier_type"`
|
||||||
Identifier string `toml:"identifier"`
|
Identifier string `toml:"identifier"`
|
||||||
DataType string `toml:"data_type"`
|
DataType string `toml:"data_type"` // Kept for backward compatibility but was never used.
|
||||||
Description string `toml:"description"`
|
Description string `toml:"description"` // Kept for backward compatibility but was never used.
|
||||||
|
TagsSlice [][]string `toml:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
tag NodeSettings
|
||||||
|
idStr string
|
||||||
|
metricName string
|
||||||
|
metricTags map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupSettings struct {
|
||||||
|
MetricName string `toml:"name"` // Overrides plugin's setting
|
||||||
|
Namespace string `toml:"namespace"` // Can be overridden by node setting
|
||||||
|
IdentifierType string `toml:"identifier_type"` // Can be overridden by node setting
|
||||||
|
Nodes []NodeSettings `toml:"nodes"`
|
||||||
|
TagsSlice [][]string `toml:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPCData type
|
// OPCData type
|
||||||
|
|
@ -81,9 +99,8 @@ const (
|
||||||
|
|
||||||
const description = `Retrieve data from OPCUA devices`
|
const description = `Retrieve data from OPCUA devices`
|
||||||
const sampleConfig = `
|
const sampleConfig = `
|
||||||
[[inputs.opcua]]
|
## Metric name
|
||||||
## Device name
|
# name = "opcua"
|
||||||
# name = "localhost"
|
|
||||||
#
|
#
|
||||||
## OPC UA Endpoint URL
|
## OPC UA Endpoint URL
|
||||||
# endpoint = "opc.tcp://localhost:4840"
|
# endpoint = "opc.tcp://localhost:4840"
|
||||||
|
|
@ -120,18 +137,41 @@ const sampleConfig = `
|
||||||
# password = ""
|
# password = ""
|
||||||
#
|
#
|
||||||
## Node ID configuration
|
## Node ID configuration
|
||||||
## name - the variable name
|
## name - field name to use in the output
|
||||||
## namespace - integer value 0 thru 3
|
## namespace - OPC UA namespace of the node (integer value 0 thru 3)
|
||||||
## identifier_type - s=string, i=numeric, g=guid, b=opaque
|
## identifier_type - OPC UA ID type (s=string, i=numeric, g=guid, b=opaque)
|
||||||
## identifier - tag as shown in opcua browser
|
## identifier - OPC UA ID (tag as shown in opcua browser)
|
||||||
## data_type - boolean, byte, short, int, uint, uint16, int16,
|
|
||||||
## uint32, int32, float, double, string, datetime, number
|
|
||||||
## Example:
|
## Example:
|
||||||
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"}
|
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262"}
|
||||||
nodes = [
|
# nodes = [
|
||||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
]
|
#]
|
||||||
|
#
|
||||||
|
## Node Group
|
||||||
|
## Sets defaults for OPC UA namespace and ID type so they aren't required in
|
||||||
|
## every node. A group can also have a metric name that overrides the main
|
||||||
|
## plugin metric name.
|
||||||
|
##
|
||||||
|
## Multiple node groups are allowed
|
||||||
|
#[[inputs.opcua.group]]
|
||||||
|
## Group Metric name. Overrides the top level name. If unset, the
|
||||||
|
## top level name is used.
|
||||||
|
# name =
|
||||||
|
#
|
||||||
|
## Group default namespace. If a node in the group doesn't set its
|
||||||
|
## namespace, this is used.
|
||||||
|
# namespace =
|
||||||
|
#
|
||||||
|
## Group default identifier type. If a node in the group doesn't set its
|
||||||
|
## namespace, this is used.
|
||||||
|
# identifier_type =
|
||||||
|
#
|
||||||
|
## Node ID Configuration. Array of nodes with the same settings as above.
|
||||||
|
# nodes = [
|
||||||
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
|
# {name="", namespace="", identifier_type="", identifier=""},
|
||||||
|
#]
|
||||||
`
|
`
|
||||||
|
|
||||||
// Description will appear directly above the plugin definition in the config file
|
// Description will appear directly above the plugin definition in the config file
|
||||||
|
|
@ -157,16 +197,21 @@ func (o *OpcUA) Init() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
o.NumberOfTags = len(o.NodeList)
|
|
||||||
|
|
||||||
o.setupOptions()
|
o.setupOptions()
|
||||||
|
|
||||||
|
tags := map[string]string{
|
||||||
|
"endpoint": o.Endpoint,
|
||||||
|
}
|
||||||
|
o.ReadError = selfstat.Register("opcua", "read_error", tags)
|
||||||
|
o.ReadSuccess = selfstat.Register("opcua", "read_success", tags)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpcUA) validateEndpoint() error {
|
func (o *OpcUA) validateEndpoint() error {
|
||||||
if o.Name == "" {
|
if o.MetricName == "" {
|
||||||
return fmt.Errorf("device name is empty")
|
return fmt.Errorf("device name is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,22 +229,79 @@ func (o *OpcUA) validateEndpoint() error {
|
||||||
case "None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "auto":
|
case "None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "auto":
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityPolicy, o.Name)
|
return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityPolicy, o.MetricName)
|
||||||
}
|
}
|
||||||
//search security mode type
|
//search security mode type
|
||||||
switch o.SecurityMode {
|
switch o.SecurityMode {
|
||||||
case "None", "Sign", "SignAndEncrypt", "auto":
|
case "None", "Sign", "SignAndEncrypt", "auto":
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityMode, o.Name)
|
return fmt.Errorf("invalid security type '%s' in '%s'", o.SecurityMode, o.MetricName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tagsSliceToMap(tags [][]string) (map[string]string, error) {
|
||||||
|
m := make(map[string]string)
|
||||||
|
for i, tag := range tags {
|
||||||
|
if len(tag) != 2 {
|
||||||
|
return nil, fmt.Errorf("tag %d needs 2 values, has %d: %v", i+1, len(tag), tag)
|
||||||
|
}
|
||||||
|
if tag[0] == "" {
|
||||||
|
return nil, fmt.Errorf("tag %d has empty name", i+1)
|
||||||
|
}
|
||||||
|
if tag[1] == "" {
|
||||||
|
return nil, fmt.Errorf("tag %d has empty value", i+1)
|
||||||
|
}
|
||||||
|
if _, ok := m[tag[0]]; ok {
|
||||||
|
return nil, fmt.Errorf("tag %d has duplicate key: %v", i+1, tag[0])
|
||||||
|
}
|
||||||
|
m[tag[0]] = tag[1]
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
//InitNodes Method on OpcUA
|
//InitNodes Method on OpcUA
|
||||||
func (o *OpcUA) InitNodes() error {
|
func (o *OpcUA) InitNodes() error {
|
||||||
if len(o.NodeList) == 0 {
|
for _, node := range o.RootNodes {
|
||||||
return nil
|
o.nodes = append(o.nodes, Node{
|
||||||
|
metricName: o.MetricName,
|
||||||
|
tag: node,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range o.Groups {
|
||||||
|
if group.MetricName == "" {
|
||||||
|
group.MetricName = o.MetricName
|
||||||
|
}
|
||||||
|
groupTags, err := tagsSliceToMap(group.TagsSlice)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, node := range group.Nodes {
|
||||||
|
if node.Namespace == "" {
|
||||||
|
node.Namespace = group.Namespace
|
||||||
|
}
|
||||||
|
if node.IdentifierType == "" {
|
||||||
|
node.IdentifierType = group.IdentifierType
|
||||||
|
}
|
||||||
|
nodeTags, err := tagsSliceToMap(node.TagsSlice)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mergedTags := make(map[string]string)
|
||||||
|
for k, v := range groupTags {
|
||||||
|
mergedTags[k] = v
|
||||||
|
}
|
||||||
|
for k, v := range nodeTags {
|
||||||
|
mergedTags[k] = v
|
||||||
|
}
|
||||||
|
o.nodes = append(o.nodes, Node{
|
||||||
|
metricName: group.MetricName,
|
||||||
|
tag: node,
|
||||||
|
metricTags: mergedTags,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := o.validateOPCTags()
|
err := o.validateOPCTags()
|
||||||
|
|
@ -210,50 +312,74 @@ func (o *OpcUA) InitNodes() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type metricParts struct {
|
||||||
|
metricName string
|
||||||
|
fieldName string
|
||||||
|
tags string // sorted by tag name and in format tag1=value1, tag2=value2
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMP(n *Node) metricParts {
|
||||||
|
var keys []string
|
||||||
|
for key := range n.metricTags {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, key := range keys {
|
||||||
|
if i != 0 {
|
||||||
|
sb.WriteString(", ")
|
||||||
|
}
|
||||||
|
sb.WriteString(key)
|
||||||
|
sb.WriteString("=")
|
||||||
|
sb.WriteString(n.metricTags[key])
|
||||||
|
}
|
||||||
|
x := metricParts{
|
||||||
|
metricName: n.metricName,
|
||||||
|
fieldName: n.tag.FieldName,
|
||||||
|
tags: sb.String(),
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpcUA) validateOPCTags() error {
|
func (o *OpcUA) validateOPCTags() error {
|
||||||
nameEncountered := map[string]bool{}
|
nameEncountered := map[metricParts]struct{}{}
|
||||||
for i, item := range o.NodeList {
|
for _, node := range o.nodes {
|
||||||
|
mp := newMP(&node)
|
||||||
//check empty name
|
//check empty name
|
||||||
if item.Name == "" {
|
if node.tag.FieldName == "" {
|
||||||
return fmt.Errorf("empty name in '%s'", item.Name)
|
return fmt.Errorf("empty name in '%s'", node.tag.FieldName)
|
||||||
}
|
}
|
||||||
//search name duplicate
|
//search name duplicate
|
||||||
if nameEncountered[item.Name] {
|
if _, ok := nameEncountered[mp]; ok {
|
||||||
return fmt.Errorf("name '%s' is duplicated in '%s'", item.Name, item.Name)
|
return fmt.Errorf("name '%s' is duplicated (metric name '%s', tags '%s')",
|
||||||
|
mp.fieldName, mp.metricName, mp.tags)
|
||||||
} else {
|
} else {
|
||||||
nameEncountered[item.Name] = true
|
//add it to the set
|
||||||
|
nameEncountered[mp] = struct{}{}
|
||||||
}
|
}
|
||||||
//search identifier type
|
//search identifier type
|
||||||
switch item.IdentifierType {
|
switch node.tag.IdentifierType {
|
||||||
case "s", "i", "g", "b":
|
case "s", "i", "g", "b":
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid identifier type '%s' in '%s'", item.IdentifierType, item.Name)
|
return fmt.Errorf("invalid identifier type '%s' in '%s'", node.tag.IdentifierType, node.tag.FieldName)
|
||||||
}
|
|
||||||
// search data type
|
|
||||||
switch item.DataType {
|
|
||||||
case "boolean", "byte", "short", "int", "uint", "uint16", "int16", "uint32", "int32", "float", "double", "string", "datetime", "number":
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid data type '%s' in '%s'", item.DataType, item.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// build nodeid
|
node.idStr = BuildNodeID(node.tag)
|
||||||
o.Nodes = append(o.Nodes, BuildNodeID(item))
|
|
||||||
|
|
||||||
//parse NodeIds and NodeIds errors
|
//parse NodeIds and NodeIds errors
|
||||||
nid, niderr := ua.ParseNodeID(o.Nodes[i])
|
nid, niderr := ua.ParseNodeID(node.idStr)
|
||||||
// build NodeIds and Errors
|
// build NodeIds and Errors
|
||||||
o.NodeIDs = append(o.NodeIDs, nid)
|
o.nodeIDs = append(o.nodeIDs, nid)
|
||||||
o.NodeIDerror = append(o.NodeIDerror, niderr)
|
o.nodeIDerror = append(o.nodeIDerror, niderr)
|
||||||
// Grow NodeData for later input
|
// Grow NodeData for later input
|
||||||
o.NodeData = append(o.NodeData, OPCData{})
|
o.nodeData = append(o.nodeData, OPCData{})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildNodeID build node ID from OPC tag
|
// BuildNodeID build node ID from OPC tag
|
||||||
func BuildNodeID(tag OPCTag) string {
|
func BuildNodeID(tag NodeSettings) string {
|
||||||
return "ns=" + tag.Namespace + ";" + tag.IdentifierType + "=" + tag.Identifier
|
return "ns=" + tag.Namespace + ";" + tag.IdentifierType + "=" + tag.Identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +406,7 @@ func Connect(o *OpcUA) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
regResp, err := o.client.RegisterNodes(&ua.RegisterNodesRequest{
|
regResp, err := o.client.RegisterNodes(&ua.RegisterNodesRequest{
|
||||||
NodesToRegister: o.NodeIDs,
|
NodesToRegister: o.nodeIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("RegisterNodes failed: %v", err)
|
return fmt.Errorf("RegisterNodes failed: %v", err)
|
||||||
|
|
@ -325,22 +451,22 @@ func (o *OpcUA) setupOptions() error {
|
||||||
func (o *OpcUA) getData() error {
|
func (o *OpcUA) getData() error {
|
||||||
resp, err := o.client.Read(o.req)
|
resp, err := o.client.Read(o.req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.ReadError++
|
o.ReadError.Incr(1)
|
||||||
return fmt.Errorf("RegisterNodes Read failed: %v", err)
|
return fmt.Errorf("RegisterNodes Read failed: %v", err)
|
||||||
}
|
}
|
||||||
o.ReadSuccess++
|
o.ReadSuccess.Incr(1)
|
||||||
for i, d := range resp.Results {
|
for i, d := range resp.Results {
|
||||||
if d.Status != ua.StatusOK {
|
if d.Status != ua.StatusOK {
|
||||||
return fmt.Errorf("Status not OK: %v", d.Status)
|
return fmt.Errorf("Status not OK: %v", d.Status)
|
||||||
}
|
}
|
||||||
o.NodeData[i].TagName = o.NodeList[i].Name
|
o.nodeData[i].TagName = o.nodes[i].tag.FieldName
|
||||||
if d.Value != nil {
|
if d.Value != nil {
|
||||||
o.NodeData[i].Value = d.Value.Value()
|
o.nodeData[i].Value = d.Value.Value()
|
||||||
o.NodeData[i].DataType = d.Value.Type()
|
o.nodeData[i].DataType = d.Value.Type()
|
||||||
}
|
}
|
||||||
o.NodeData[i].Quality = d.Status
|
o.nodeData[i].Quality = d.Status
|
||||||
o.NodeData[i].TimeStamp = d.ServerTimestamp.String()
|
o.nodeData[i].TimeStamp = d.ServerTimestamp.String()
|
||||||
o.NodeData[i].Time = d.SourceTimestamp.String()
|
o.nodeData[i].Time = d.SourceTimestamp.String()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -359,9 +485,6 @@ func disconnect(o *OpcUA) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
o.ReadError = 0
|
|
||||||
o.ReadSuccess = 0
|
|
||||||
|
|
||||||
switch u.Scheme {
|
switch u.Scheme {
|
||||||
case "opc.tcp":
|
case "opc.tcp":
|
||||||
o.state = Disconnected
|
o.state = Disconnected
|
||||||
|
|
@ -392,16 +515,18 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, n := range o.NodeList {
|
for i, n := range o.nodes {
|
||||||
fields := make(map[string]interface{})
|
fields := make(map[string]interface{})
|
||||||
tags := map[string]string{
|
tags := map[string]string{
|
||||||
"name": n.Name,
|
"id": n.idStr,
|
||||||
"id": BuildNodeID(n),
|
}
|
||||||
|
for k, v := range n.metricTags {
|
||||||
|
tags[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
fields[o.NodeData[i].TagName] = o.NodeData[i].Value
|
fields[o.nodeData[i].TagName] = o.nodeData[i].Value
|
||||||
fields["Quality"] = strings.TrimSpace(fmt.Sprint(o.NodeData[i].Quality))
|
fields["Quality"] = strings.TrimSpace(fmt.Sprint(o.nodeData[i].Quality))
|
||||||
acc.AddFields(o.Name, fields, tags)
|
acc.AddFields(n.metricName, fields, tags)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -410,7 +535,7 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
|
||||||
func init() {
|
func init() {
|
||||||
inputs.Add("opcua", func() telegraf.Input {
|
inputs.Add("opcua", func() telegraf.Input {
|
||||||
return &OpcUA{
|
return &OpcUA{
|
||||||
Name: "localhost",
|
MetricName: "opcua",
|
||||||
Endpoint: "opc.tcp://localhost:4840",
|
Endpoint: "opc.tcp://localhost:4840",
|
||||||
SecurityPolicy: "auto",
|
SecurityPolicy: "auto",
|
||||||
SecurityMode: "auto",
|
SecurityMode: "auto",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,7 +16,6 @@ type OPCTags struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
IdentifierType string
|
IdentifierType string
|
||||||
Identifier string
|
Identifier string
|
||||||
DataType string
|
|
||||||
Want string
|
Want string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,15 +25,15 @@ func TestClient1(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var testopctags = []OPCTags{
|
var testopctags = []OPCTags{
|
||||||
{"ProductName", "0", "i", "2261", "string", "open62541 OPC UA Server"},
|
{"ProductName", "0", "i", "2261", "open62541 OPC UA Server"},
|
||||||
{"ProductUri", "0", "i", "2262", "string", "http://open62541.org"},
|
{"ProductUri", "0", "i", "2262", "http://open62541.org"},
|
||||||
{"ManufacturerName", "0", "i", "2263", "string", "open62541"},
|
{"ManufacturerName", "0", "i", "2263", "open62541"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var o OpcUA
|
var o OpcUA
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
o.Name = "testing"
|
o.MetricName = "testing"
|
||||||
o.Endpoint = "opc.tcp://opcua.rocks:4840"
|
o.Endpoint = "opc.tcp://opcua.rocks:4840"
|
||||||
o.AuthMethod = "Anonymous"
|
o.AuthMethod = "Anonymous"
|
||||||
o.ConnectTimeout = config.Duration(10 * time.Second)
|
o.ConnectTimeout = config.Duration(10 * time.Second)
|
||||||
|
|
@ -41,7 +41,7 @@ func TestClient1(t *testing.T) {
|
||||||
o.SecurityPolicy = "None"
|
o.SecurityPolicy = "None"
|
||||||
o.SecurityMode = "None"
|
o.SecurityMode = "None"
|
||||||
for _, tags := range testopctags {
|
for _, tags := range testopctags {
|
||||||
o.NodeList = append(o.NodeList, MapOPCTag(tags))
|
o.RootNodes = append(o.RootNodes, MapOPCTag(tags))
|
||||||
}
|
}
|
||||||
err = o.Init()
|
err = o.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -52,26 +52,25 @@ func TestClient1(t *testing.T) {
|
||||||
t.Fatalf("Connect Error: %s", err)
|
t.Fatalf("Connect Error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, v := range o.NodeData {
|
for i, v := range o.nodeData {
|
||||||
if v.Value != nil {
|
if v.Value != nil {
|
||||||
types := reflect.TypeOf(v.Value)
|
types := reflect.TypeOf(v.Value)
|
||||||
value := reflect.ValueOf(v.Value)
|
value := reflect.ValueOf(v.Value)
|
||||||
compare := fmt.Sprintf("%v", value.Interface())
|
compare := fmt.Sprintf("%v", value.Interface())
|
||||||
if compare != testopctags[i].Want {
|
if compare != testopctags[i].Want {
|
||||||
t.Errorf("Tag %s: Values %v for type %s does not match record", o.NodeList[i].Name, value.Interface(), types)
|
t.Errorf("Tag %s: Values %v for type %s does not match record", o.nodes[i].tag.FieldName, value.Interface(), types)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("Tag: %s has value: %v", o.NodeList[i].Name, v.Value)
|
t.Errorf("Tag: %s has value: %v", o.nodes[i].tag.FieldName, v.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MapOPCTag(tags OPCTags) (out OPCTag) {
|
func MapOPCTag(tags OPCTags) (out NodeSettings) {
|
||||||
out.Name = tags.Name
|
out.FieldName = tags.Name
|
||||||
out.Namespace = tags.Namespace
|
out.Namespace = tags.Namespace
|
||||||
out.IdentifierType = tags.IdentifierType
|
out.IdentifierType = tags.IdentifierType
|
||||||
out.Identifier = tags.Identifier
|
out.Identifier = tags.Identifier
|
||||||
out.DataType = tags.DataType
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,9 +89,21 @@ auth_method = "Anonymous"
|
||||||
username = ""
|
username = ""
|
||||||
password = ""
|
password = ""
|
||||||
nodes = [
|
nodes = [
|
||||||
{name="name", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
{name="name", namespace="1", identifier_type="s", identifier="one"},
|
||||||
{name="name2", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
{name="name2", namespace="2", identifier_type="s", identifier="two"},
|
||||||
]
|
]
|
||||||
|
[[inputs.opcua.group]]
|
||||||
|
name = "foo"
|
||||||
|
namespace = "3"
|
||||||
|
identifier_type = "i"
|
||||||
|
tags = [["tag1", "val1"], ["tag2", "val2"]]
|
||||||
|
nodes = [{name="name3", identifier="3000", tags=[["tag3", "val3"]]}]
|
||||||
|
[[inputs.opcua.group]]
|
||||||
|
name = "bar"
|
||||||
|
namespace = "0"
|
||||||
|
identifier_type = "i"
|
||||||
|
tags = [["tag1", "val1"], ["tag2", "val2"]]
|
||||||
|
nodes = [{name="name4", identifier="4000", tags=[["tag1", "override"]]}]
|
||||||
`
|
`
|
||||||
|
|
||||||
c := config.NewConfig()
|
c := config.NewConfig()
|
||||||
|
|
@ -104,7 +115,143 @@ nodes = [
|
||||||
o, ok := c.Inputs[0].Input.(*OpcUA)
|
o, ok := c.Inputs[0].Input.(*OpcUA)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
require.Len(t, o.NodeList, 2)
|
require.Len(t, o.RootNodes, 2)
|
||||||
require.Equal(t, o.NodeList[0].Name, "name")
|
require.Equal(t, o.RootNodes[0].FieldName, "name")
|
||||||
require.Equal(t, o.NodeList[1].Name, "name2")
|
require.Equal(t, o.RootNodes[1].FieldName, "name2")
|
||||||
|
|
||||||
|
require.Len(t, o.Groups, 2)
|
||||||
|
require.Equal(t, o.Groups[0].MetricName, "foo")
|
||||||
|
require.Len(t, o.Groups[0].Nodes, 1)
|
||||||
|
require.Equal(t, o.Groups[0].Nodes[0].Identifier, "3000")
|
||||||
|
|
||||||
|
require.NoError(t, o.InitNodes())
|
||||||
|
require.Len(t, o.nodes, 4)
|
||||||
|
require.Len(t, o.nodes[2].metricTags, 3)
|
||||||
|
require.Len(t, o.nodes[3].metricTags, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagsSliceToMap(t *testing.T) {
|
||||||
|
m, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"baz", "bat"}})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, m, 2)
|
||||||
|
assert.Equal(t, m["foo"], "bar")
|
||||||
|
assert.Equal(t, m["baz"], "bat")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagsSliceToMap_twoStrings(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
_, err = tagsSliceToMap([][]string{{"foo", "bar", "baz"}})
|
||||||
|
assert.Error(t, err)
|
||||||
|
_, err = tagsSliceToMap([][]string{{"foo"}})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagsSliceToMap_dupeKey(t *testing.T) {
|
||||||
|
_, err := tagsSliceToMap([][]string{{"foo", "bar"}, {"foo", "bat"}})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagsSliceToMap_empty(t *testing.T) {
|
||||||
|
_, err := tagsSliceToMap([][]string{{"foo", ""}})
|
||||||
|
assert.Equal(t, fmt.Errorf("tag 1 has empty value"), err)
|
||||||
|
_, err = tagsSliceToMap([][]string{{"", "bar"}})
|
||||||
|
assert.Equal(t, fmt.Errorf("tag 1 has empty name"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateOPCTags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nodes []Node
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"same",
|
||||||
|
[]Node{
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "v1", "t2": "v2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "v1", "t2": "v2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fmt.Errorf("name 'fn' is duplicated (metric name 'mn', tags 't1=v1, t2=v2')"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"different metric tag names",
|
||||||
|
[]Node{
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "", "t2": ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "", "t3": ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"different metric tag values",
|
||||||
|
[]Node{
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "foo", "t2": ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "bar", "t2": ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"different metric names",
|
||||||
|
[]Node{
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "", "t2": ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metricName: "mn2",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "", "t2": ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"different field names",
|
||||||
|
[]Node{
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "", "t2": ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metricName: "mn",
|
||||||
|
tag: NodeSettings{FieldName: "fn2", IdentifierType: "s"},
|
||||||
|
metricTags: map[string]string{"t1": "", "t2": ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
o := OpcUA{
|
||||||
|
nodes: tt.nodes,
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.err, o.validateOPCTags())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue