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
|
||||
[[inputs.opcua]]
|
||||
## Device name
|
||||
# name = "localhost"
|
||||
## Metric name
|
||||
# name = "opcua"
|
||||
#
|
||||
## OPC UA Endpoint URL
|
||||
# endpoint = "opc.tcp://localhost:4840"
|
||||
|
|
@ -47,34 +47,97 @@ Plugin minimum tested version: 1.16
|
|||
# password = ""
|
||||
#
|
||||
## Node ID configuration
|
||||
## name - the variable name
|
||||
## namespace - integer value 0 thru 3
|
||||
## identifier_type - s=string, i=numeric, g=guid, b=opaque
|
||||
## identifier - tag as shown in opcua browser
|
||||
## data_type - boolean, byte, short, int, uint, uint16, int16,
|
||||
## uint32, int32, float, double, string, datetime, number
|
||||
## name - field name to use in the output
|
||||
## namespace - OPC UA namespace of the node (integer value 0 thru 3)
|
||||
## identifier_type - OPC UA ID type (s=string, i=numeric, g=guid, b=opaque)
|
||||
## identifier - OPC UA ID (tag as shown in opcua browser)
|
||||
## tags - extra tags to be added to the output metric (optional)
|
||||
## 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 = [
|
||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
||||
{name="name", identifier="1001", tags=[["node1_tag", "val2"]]},
|
||||
{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
|
||||
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:
|
||||
It produces metrics like these:
|
||||
```
|
||||
{name="LabelName", namespace="3", identifier_type="s", identifier="Temperature", data_type="float", description="Description of node"},
|
||||
```
|
||||
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
opcua,host=3c70aee0901e,name=Random,type=double Random=0.018158170305814902 1597820490000000000
|
||||
|
||||
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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -13,11 +14,12 @@ import (
|
|||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/plugins/inputs"
|
||||
"github.com/influxdata/telegraf/selfstat"
|
||||
)
|
||||
|
||||
// OpcUA type
|
||||
type OpcUA struct {
|
||||
Name string `toml:"name"`
|
||||
MetricName string `toml:"name"`
|
||||
Endpoint string `toml:"endpoint"`
|
||||
SecurityPolicy string `toml:"security_policy"`
|
||||
SecurityMode string `toml:"security_mode"`
|
||||
|
|
@ -28,18 +30,18 @@ type OpcUA struct {
|
|||
AuthMethod string `toml:"auth_method"`
|
||||
ConnectTimeout config.Duration `toml:"connect_timeout"`
|
||||
RequestTimeout config.Duration `toml:"request_timeout"`
|
||||
NodeList []OPCTag `toml:"nodes"`
|
||||
RootNodes []NodeSettings `toml:"nodes"`
|
||||
Groups []GroupSettings `toml:"group"`
|
||||
|
||||
Nodes []string `toml:"-"`
|
||||
NodeData []OPCData `toml:"-"`
|
||||
NodeIDs []*ua.NodeID `toml:"-"`
|
||||
NodeIDerror []error `toml:"-"`
|
||||
nodes []Node
|
||||
nodeData []OPCData
|
||||
nodeIDs []*ua.NodeID
|
||||
nodeIDerror []error
|
||||
state ConnectionState
|
||||
|
||||
// status
|
||||
ReadSuccess int `toml:"-"`
|
||||
ReadError int `toml:"-"`
|
||||
NumberOfTags int `toml:"-"`
|
||||
ReadSuccess selfstat.Stat `toml:"-"`
|
||||
ReadError selfstat.Stat `toml:"-"`
|
||||
|
||||
// internal values
|
||||
client *opcua.Client
|
||||
|
|
@ -48,13 +50,29 @@ type OpcUA struct {
|
|||
}
|
||||
|
||||
// OPCTag type
|
||||
type OPCTag struct {
|
||||
Name string `toml:"name"`
|
||||
Namespace string `toml:"namespace"`
|
||||
IdentifierType string `toml:"identifier_type"`
|
||||
Identifier string `toml:"identifier"`
|
||||
DataType string `toml:"data_type"`
|
||||
Description string `toml:"description"`
|
||||
type NodeSettings struct {
|
||||
FieldName string `toml:"name"`
|
||||
Namespace string `toml:"namespace"`
|
||||
IdentifierType string `toml:"identifier_type"`
|
||||
Identifier string `toml:"identifier"`
|
||||
DataType string `toml:"data_type"` // Kept for backward compatibility but was never used.
|
||||
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
|
||||
|
|
@ -81,9 +99,8 @@ const (
|
|||
|
||||
const description = `Retrieve data from OPCUA devices`
|
||||
const sampleConfig = `
|
||||
[[inputs.opcua]]
|
||||
## Device name
|
||||
# name = "localhost"
|
||||
## Metric name
|
||||
# name = "opcua"
|
||||
#
|
||||
## OPC UA Endpoint URL
|
||||
# endpoint = "opc.tcp://localhost:4840"
|
||||
|
|
@ -120,18 +137,41 @@ const sampleConfig = `
|
|||
# password = ""
|
||||
#
|
||||
## Node ID configuration
|
||||
## name - the variable name
|
||||
## namespace - integer value 0 thru 3
|
||||
## identifier_type - s=string, i=numeric, g=guid, b=opaque
|
||||
## identifier - tag as shown in opcua browser
|
||||
## data_type - boolean, byte, short, int, uint, uint16, int16,
|
||||
## uint32, int32, float, double, string, datetime, number
|
||||
## name - field name to use in the output
|
||||
## namespace - OPC UA namespace of the node (integer value 0 thru 3)
|
||||
## identifier_type - OPC UA ID type (s=string, i=numeric, g=guid, b=opaque)
|
||||
## identifier - OPC UA ID (tag as shown in opcua browser)
|
||||
## Example:
|
||||
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262", data_type="string", description="http://open62541.org"}
|
||||
nodes = [
|
||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
||||
{name="", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
||||
]
|
||||
## {name="ProductUri", namespace="0", identifier_type="i", identifier="2262"}
|
||||
# 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=""},
|
||||
#]
|
||||
`
|
||||
|
||||
// Description will appear directly above the plugin definition in the config file
|
||||
|
|
@ -157,16 +197,21 @@ func (o *OpcUA) Init() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.NumberOfTags = len(o.NodeList)
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
func (o *OpcUA) validateEndpoint() error {
|
||||
if o.Name == "" {
|
||||
if o.MetricName == "" {
|
||||
return fmt.Errorf("device name is empty")
|
||||
}
|
||||
|
||||
|
|
@ -184,22 +229,79 @@ func (o *OpcUA) validateEndpoint() error {
|
|||
case "None", "Basic128Rsa15", "Basic256", "Basic256Sha256", "auto":
|
||||
break
|
||||
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
|
||||
switch o.SecurityMode {
|
||||
case "None", "Sign", "SignAndEncrypt", "auto":
|
||||
break
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
func (o *OpcUA) InitNodes() error {
|
||||
if len(o.NodeList) == 0 {
|
||||
return nil
|
||||
for _, node := range o.RootNodes {
|
||||
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()
|
||||
|
|
@ -210,50 +312,74 @@ func (o *OpcUA) InitNodes() error {
|
|||
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 {
|
||||
nameEncountered := map[string]bool{}
|
||||
for i, item := range o.NodeList {
|
||||
nameEncountered := map[metricParts]struct{}{}
|
||||
for _, node := range o.nodes {
|
||||
mp := newMP(&node)
|
||||
//check empty name
|
||||
if item.Name == "" {
|
||||
return fmt.Errorf("empty name in '%s'", item.Name)
|
||||
if node.tag.FieldName == "" {
|
||||
return fmt.Errorf("empty name in '%s'", node.tag.FieldName)
|
||||
}
|
||||
//search name duplicate
|
||||
if nameEncountered[item.Name] {
|
||||
return fmt.Errorf("name '%s' is duplicated in '%s'", item.Name, item.Name)
|
||||
if _, ok := nameEncountered[mp]; ok {
|
||||
return fmt.Errorf("name '%s' is duplicated (metric name '%s', tags '%s')",
|
||||
mp.fieldName, mp.metricName, mp.tags)
|
||||
} else {
|
||||
nameEncountered[item.Name] = true
|
||||
//add it to the set
|
||||
nameEncountered[mp] = struct{}{}
|
||||
}
|
||||
//search identifier type
|
||||
switch item.IdentifierType {
|
||||
switch node.tag.IdentifierType {
|
||||
case "s", "i", "g", "b":
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("invalid identifier type '%s' in '%s'", item.IdentifierType, item.Name)
|
||||
}
|
||||
// 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)
|
||||
return fmt.Errorf("invalid identifier type '%s' in '%s'", node.tag.IdentifierType, node.tag.FieldName)
|
||||
}
|
||||
|
||||
// build nodeid
|
||||
o.Nodes = append(o.Nodes, BuildNodeID(item))
|
||||
node.idStr = BuildNodeID(node.tag)
|
||||
|
||||
//parse NodeIds and NodeIds errors
|
||||
nid, niderr := ua.ParseNodeID(o.Nodes[i])
|
||||
nid, niderr := ua.ParseNodeID(node.idStr)
|
||||
// build NodeIds and Errors
|
||||
o.NodeIDs = append(o.NodeIDs, nid)
|
||||
o.NodeIDerror = append(o.NodeIDerror, niderr)
|
||||
o.nodeIDs = append(o.nodeIDs, nid)
|
||||
o.nodeIDerror = append(o.nodeIDerror, niderr)
|
||||
// Grow NodeData for later input
|
||||
o.NodeData = append(o.NodeData, OPCData{})
|
||||
o.nodeData = append(o.nodeData, OPCData{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +406,7 @@ func Connect(o *OpcUA) error {
|
|||
}
|
||||
|
||||
regResp, err := o.client.RegisterNodes(&ua.RegisterNodesRequest{
|
||||
NodesToRegister: o.NodeIDs,
|
||||
NodesToRegister: o.nodeIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("RegisterNodes failed: %v", err)
|
||||
|
|
@ -325,22 +451,22 @@ func (o *OpcUA) setupOptions() error {
|
|||
func (o *OpcUA) getData() error {
|
||||
resp, err := o.client.Read(o.req)
|
||||
if err != nil {
|
||||
o.ReadError++
|
||||
o.ReadError.Incr(1)
|
||||
return fmt.Errorf("RegisterNodes Read failed: %v", err)
|
||||
}
|
||||
o.ReadSuccess++
|
||||
o.ReadSuccess.Incr(1)
|
||||
for i, d := range resp.Results {
|
||||
if d.Status != ua.StatusOK {
|
||||
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 {
|
||||
o.NodeData[i].Value = d.Value.Value()
|
||||
o.NodeData[i].DataType = d.Value.Type()
|
||||
o.nodeData[i].Value = d.Value.Value()
|
||||
o.nodeData[i].DataType = d.Value.Type()
|
||||
}
|
||||
o.NodeData[i].Quality = d.Status
|
||||
o.NodeData[i].TimeStamp = d.ServerTimestamp.String()
|
||||
o.NodeData[i].Time = d.SourceTimestamp.String()
|
||||
o.nodeData[i].Quality = d.Status
|
||||
o.nodeData[i].TimeStamp = d.ServerTimestamp.String()
|
||||
o.nodeData[i].Time = d.SourceTimestamp.String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -359,9 +485,6 @@ func disconnect(o *OpcUA) error {
|
|||
return err
|
||||
}
|
||||
|
||||
o.ReadError = 0
|
||||
o.ReadSuccess = 0
|
||||
|
||||
switch u.Scheme {
|
||||
case "opc.tcp":
|
||||
o.state = Disconnected
|
||||
|
|
@ -392,16 +515,18 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
|
|||
return err
|
||||
}
|
||||
|
||||
for i, n := range o.NodeList {
|
||||
for i, n := range o.nodes {
|
||||
fields := make(map[string]interface{})
|
||||
tags := map[string]string{
|
||||
"name": n.Name,
|
||||
"id": BuildNodeID(n),
|
||||
"id": n.idStr,
|
||||
}
|
||||
for k, v := range n.metricTags {
|
||||
tags[k] = v
|
||||
}
|
||||
|
||||
fields[o.NodeData[i].TagName] = o.NodeData[i].Value
|
||||
fields["Quality"] = strings.TrimSpace(fmt.Sprint(o.NodeData[i].Quality))
|
||||
acc.AddFields(o.Name, fields, tags)
|
||||
fields[o.nodeData[i].TagName] = o.nodeData[i].Value
|
||||
fields["Quality"] = strings.TrimSpace(fmt.Sprint(o.nodeData[i].Quality))
|
||||
acc.AddFields(n.metricName, fields, tags)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -410,7 +535,7 @@ func (o *OpcUA) Gather(acc telegraf.Accumulator) error {
|
|||
func init() {
|
||||
inputs.Add("opcua", func() telegraf.Input {
|
||||
return &OpcUA{
|
||||
Name: "localhost",
|
||||
MetricName: "opcua",
|
||||
Endpoint: "opc.tcp://localhost:4840",
|
||||
SecurityPolicy: "auto",
|
||||
SecurityMode: "auto",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -15,7 +16,6 @@ type OPCTags struct {
|
|||
Namespace string
|
||||
IdentifierType string
|
||||
Identifier string
|
||||
DataType string
|
||||
Want string
|
||||
}
|
||||
|
||||
|
|
@ -25,15 +25,15 @@ func TestClient1(t *testing.T) {
|
|||
}
|
||||
|
||||
var testopctags = []OPCTags{
|
||||
{"ProductName", "0", "i", "2261", "string", "open62541 OPC UA Server"},
|
||||
{"ProductUri", "0", "i", "2262", "string", "http://open62541.org"},
|
||||
{"ManufacturerName", "0", "i", "2263", "string", "open62541"},
|
||||
{"ProductName", "0", "i", "2261", "open62541 OPC UA Server"},
|
||||
{"ProductUri", "0", "i", "2262", "http://open62541.org"},
|
||||
{"ManufacturerName", "0", "i", "2263", "open62541"},
|
||||
}
|
||||
|
||||
var o OpcUA
|
||||
var err error
|
||||
|
||||
o.Name = "testing"
|
||||
o.MetricName = "testing"
|
||||
o.Endpoint = "opc.tcp://opcua.rocks:4840"
|
||||
o.AuthMethod = "Anonymous"
|
||||
o.ConnectTimeout = config.Duration(10 * time.Second)
|
||||
|
|
@ -41,7 +41,7 @@ func TestClient1(t *testing.T) {
|
|||
o.SecurityPolicy = "None"
|
||||
o.SecurityMode = "None"
|
||||
for _, tags := range testopctags {
|
||||
o.NodeList = append(o.NodeList, MapOPCTag(tags))
|
||||
o.RootNodes = append(o.RootNodes, MapOPCTag(tags))
|
||||
}
|
||||
err = o.Init()
|
||||
if err != nil {
|
||||
|
|
@ -52,26 +52,25 @@ func TestClient1(t *testing.T) {
|
|||
t.Fatalf("Connect Error: %s", err)
|
||||
}
|
||||
|
||||
for i, v := range o.NodeData {
|
||||
for i, v := range o.nodeData {
|
||||
if v.Value != nil {
|
||||
types := reflect.TypeOf(v.Value)
|
||||
value := reflect.ValueOf(v.Value)
|
||||
compare := fmt.Sprintf("%v", value.Interface())
|
||||
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 {
|
||||
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) {
|
||||
out.Name = tags.Name
|
||||
func MapOPCTag(tags OPCTags) (out NodeSettings) {
|
||||
out.FieldName = tags.Name
|
||||
out.Namespace = tags.Namespace
|
||||
out.IdentifierType = tags.IdentifierType
|
||||
out.Identifier = tags.Identifier
|
||||
out.DataType = tags.DataType
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
@ -90,9 +89,21 @@ auth_method = "Anonymous"
|
|||
username = ""
|
||||
password = ""
|
||||
nodes = [
|
||||
{name="name", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
||||
{name="name2", namespace="", identifier_type="", identifier="", data_type="", description=""},
|
||||
{name="name", namespace="1", identifier_type="s", identifier="one"},
|
||||
{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()
|
||||
|
|
@ -104,7 +115,143 @@ nodes = [
|
|||
o, ok := c.Inputs[0].Input.(*OpcUA)
|
||||
require.True(t, ok)
|
||||
|
||||
require.Len(t, o.NodeList, 2)
|
||||
require.Equal(t, o.NodeList[0].Name, "name")
|
||||
require.Equal(t, o.NodeList[1].Name, "name2")
|
||||
require.Len(t, o.RootNodes, 2)
|
||||
require.Equal(t, o.RootNodes[0].FieldName, "name")
|
||||
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