221 lines
5.2 KiB
Go
221 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/xml"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
type Project struct {
|
|
XMLName xml.Name `xml:"Project"`
|
|
ChannelList []Channel `xml:"ChannelList>Channel"`
|
|
}
|
|
|
|
type Channel struct {
|
|
Name string `xml:"Name"`
|
|
DeviceList []Device `xml:"DeviceList>Device"`
|
|
}
|
|
|
|
type Device struct {
|
|
Name string `xml:"Name"`
|
|
TagList []Tag `xml:"TagList>Tag"`
|
|
}
|
|
|
|
type Tag struct {
|
|
Name string `xml:"Name"`
|
|
Description string `xml:"Description"`
|
|
DataType string `xml:"DataType"`
|
|
}
|
|
|
|
type TelegrafConfig struct {
|
|
Inputs struct {
|
|
OPCUA []struct {
|
|
Nodes []map[string]interface{} `toml:"nodes"`
|
|
} `toml:"inputs.opcua"`
|
|
} `toml:"inputs"`
|
|
}
|
|
|
|
func printStatistics(project Project) {
|
|
groupSet := make(map[string]bool)
|
|
roomSet := make(map[string]bool)
|
|
|
|
for _, channel := range project.ChannelList {
|
|
groupSet[channel.Name] = true
|
|
for _, device := range channel.DeviceList {
|
|
roomSet[fmt.Sprintf("%s.%s", channel.Name, device.Name)] = true
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Statistics:\n")
|
|
fmt.Printf(" Number of groups: %d\n", len(groupSet))
|
|
fmt.Printf(" Number of rooms: %d\n", len(roomSet))
|
|
}
|
|
|
|
func main() {
|
|
// Parse CLI args
|
|
inputPath := flag.String("telegraf_template", "", "Input telegraf.conf file")
|
|
outputPath := flag.String("telegraf_out", "", "Output telegraf.conf file")
|
|
csvPath := flag.String("csv_out", "", "Write documentation to CSV file")
|
|
|
|
flag.Parse()
|
|
|
|
if flag.NArg() != 1 {
|
|
fmt.Println("Usage: program <kepware_export.xml>")
|
|
flag.PrintDefaults()
|
|
return
|
|
}
|
|
|
|
// Need either both or neither of telegraf input/output
|
|
if *inputPath != "" && *outputPath == "" || *inputPath == "" && *outputPath != "" {
|
|
fmt.Println("Error: Either both or neither of -telegraf_template and -output_file must be provided.")
|
|
return
|
|
}
|
|
|
|
kepwareXML := flag.Arg(0)
|
|
|
|
// Read the XML file
|
|
xmlFile, err := os.Open(kepwareXML)
|
|
if err != nil {
|
|
fmt.Println("Error opening file:", err)
|
|
return
|
|
}
|
|
defer xmlFile.Close()
|
|
|
|
byteValue, _ := io.ReadAll(xmlFile)
|
|
|
|
var project Project
|
|
xml.Unmarshal(byteValue, &project)
|
|
|
|
/*
|
|
Telegraf config generation
|
|
*/
|
|
|
|
if *inputPath != "" {
|
|
|
|
// Read the base Telegraf config
|
|
var config map[string]interface{}
|
|
if _, err := toml.DecodeFile(*inputPath, &config); err != nil {
|
|
fmt.Println("Error reading base config:", err)
|
|
return
|
|
}
|
|
|
|
// Print statistics
|
|
printStatistics(project)
|
|
|
|
// Generate node definitions
|
|
var nodes []map[string]interface{}
|
|
for _, channel := range project.ChannelList {
|
|
for _, device := range channel.DeviceList {
|
|
for _, tag := range device.TagList {
|
|
|
|
// Group "Razno" are general PLCs, not bound to specific rooms, so we use "plc" instead of "room"
|
|
plc_tag := "room"
|
|
if channel.Name == "Razno" {
|
|
plc_tag = "plc"
|
|
}
|
|
|
|
node := map[string]interface{}{
|
|
"name": tag.Name,
|
|
"identifier_type": "s",
|
|
"namespace": "2",
|
|
"identifier": fmt.Sprintf("%s.%s.%s", channel.Name, device.Name, tag.Name),
|
|
"tags": [][]string{
|
|
{"group", channel.Name},
|
|
{plc_tag, device.Name},
|
|
},
|
|
}
|
|
nodes = append(nodes, node)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Printf(" Total number of nodes: %d\n", len(nodes))
|
|
|
|
// Update the config
|
|
inputs, ok := config["inputs"].(map[string]interface{})
|
|
if !ok {
|
|
fmt.Println("Error: 'inputs' section not found in config")
|
|
return
|
|
}
|
|
|
|
opcuaInputs, ok := inputs["opcua_listener"].([]map[string]interface{})
|
|
if !ok || len(opcuaInputs) == 0 {
|
|
fmt.Println("Warn: 'inputs.opcua_listener' section not found in config")
|
|
// return
|
|
} else {
|
|
opcuaInputs[0]["nodes"] = nodes
|
|
}
|
|
|
|
opcuaListenerInputs, ok := inputs["opcua"].([]map[string]interface{})
|
|
if !ok || len(opcuaListenerInputs) == 0 {
|
|
fmt.Println("Warn: 'inputs.opcua' section not found in config")
|
|
// return
|
|
} else {
|
|
opcuaListenerInputs[0]["nodes"] = nodes
|
|
}
|
|
|
|
// Write the updated config to a new file
|
|
f, err := os.Create(*outputPath)
|
|
if err != nil {
|
|
fmt.Println("Error creating output file:", err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
encoder := toml.NewEncoder(f)
|
|
encoder.Indent = " "
|
|
if err := encoder.Encode(config); err != nil {
|
|
fmt.Println("Error encoding TOML:", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("Config written to %s\n", *outputPath)
|
|
}
|
|
|
|
/*
|
|
Generate CSV documentation (optional)
|
|
*/
|
|
if *csvPath != "" {
|
|
uniqueTags := make(map[string]Tag)
|
|
for _, channel := range project.ChannelList {
|
|
for _, device := range channel.DeviceList {
|
|
for _, tag := range device.TagList {
|
|
uniqueTags[fmt.Sprintf("%s.%s.%s", tag.Name, tag.Description, tag.DataType)] = tag
|
|
}
|
|
}
|
|
}
|
|
fmt.Printf(" Total number of unique tags: %d\n", len(uniqueTags))
|
|
|
|
// Sort the tags alphabetically by name
|
|
keys := make([]string, 0, len(uniqueTags))
|
|
for k := range uniqueTags {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
// Write the CSV file
|
|
f, err := os.Create(*csvPath)
|
|
if err != nil {
|
|
fmt.Println("Error creating CSV file:", err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
writer := csv.NewWriter(f)
|
|
defer writer.Flush()
|
|
|
|
writer.Write([]string{"DataType", "Name", "Description"})
|
|
for _, k := range keys {
|
|
writer.Write([]string{uniqueTags[k].DataType, uniqueTags[k].Name, uniqueTags[k].Description})
|
|
}
|
|
|
|
fmt.Printf("CSV documentation written to %s\n", *csvPath)
|
|
}
|
|
|
|
}
|