Skip to content

Commit e25d501

Browse files
author
Dennis Labordus
committed
First version of exporting IED Information to a CSV File.
Signed-off-by: Dennis Labordus <[email protected]>
1 parent fb4ea0a commit e25d501

File tree

4 files changed

+303
-6
lines changed

4 files changed

+303
-6
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@material/mwc-textfield": "0.22.1",
3838
"@material/mwc-top-app-bar-fixed": "0.22.1",
3939
"ace-custom-element": "^1.6.5",
40+
"csv-stringify": "^6.2.0",
4041
"lit-element": "2.5.1",
4142
"lit-html": "1.4.1",
4243
"lit-translate": "^1.2.1",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
"comments": [
3+
"This file contains configuration for exporting IED Information to a CSV File.",
4+
"Each column can be defined below in the section \"columns\".",
5+
"A Column must at least have a \"header\" defined.",
6+
"",
7+
"A selector can be defined to search for a Element, if no selector is defined, the IED Element is used.",
8+
"If the useOwnerDocument is set to true, the selector will be used on the whole document, otherwise the IED Element",
9+
"",
10+
"If a attributeName is defined that attribute will be retrieved from the elements found by the selector.",
11+
"If a dataAttributePath is defined, the selector should return a LN(0) Element and the path is then used to search for a DAI/DA Value."
12+
],
13+
"columns": [
14+
{
15+
"header": "IED Name",
16+
"attributeName": "name"
17+
},
18+
{
19+
"header": "IP address",
20+
"selector": "Communication > SubNetwork > ConnectedAP[iedName=\"{{ iedName }}\"] > Address:first-child > P[type=\"IP\"]",
21+
"useOwnerDocument": true
22+
},
23+
{
24+
"header": "Subnetmask",
25+
"selector": "Communication > SubNetwork > ConnectedAP[iedName=\"{{ iedName }}\"] > Address:first-child > P[type=\"IP-SUBNET\"]",
26+
"useOwnerDocument": true
27+
},
28+
{
29+
"header": "IED Description",
30+
"attributeName": "desc"
31+
},
32+
{
33+
"header": "IL1 Primary rated current",
34+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"IL1\"][lnClass=\"TCTR\"]",
35+
"dataAttributePath": ["ARtg", "setMag", "f"]
36+
},
37+
{
38+
"header": "IL1 Network Nominal Current",
39+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"IL1\"][lnClass=\"TCTR\"]",
40+
"dataAttributePath": ["ARtgNom", "setMag", "f"]
41+
},
42+
{
43+
"header": "IL1 Secondary rated current",
44+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"IL1\"][lnClass=\"TCTR\"]",
45+
"dataAttributePath": ["ARtgSec", "setVal"]
46+
},
47+
{
48+
"header": "IL1 Rated Secondary Value",
49+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"IL1\"][lnClass=\"TCTR\"]",
50+
"dataAttributePath": ["VRtgSec", "setMag", "f"]
51+
},
52+
{
53+
"header": "RES Primary rated current",
54+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"RES\"][lnClass=\"TCTR\"]",
55+
"dataAttributePath": ["ARtg", "setMag", "f"]
56+
},
57+
{
58+
"header": "RES Network Nominal Current",
59+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"RES\"][lnClass=\"TCTR\"]",
60+
"dataAttributePath": ["ARtgNom", "setMag", "f"]
61+
},
62+
{
63+
"header": "RES Secondary rated current",
64+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"RES\"][lnClass=\"TCTR\"]",
65+
"dataAttributePath": ["ARtgSec", "setVal"]
66+
},
67+
{
68+
"header": "RES Rated Secondary Value",
69+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[prefix=\"RES\"][lnClass=\"TCTR\"]",
70+
"dataAttributePath": ["VRtgSec", "setMag", "f"]
71+
},
72+
{
73+
"header": "UL1 Primary rated voltage",
74+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[lnClass=\"TVTR\"]",
75+
"dataAttributePath": ["VRtg", "setMag", "f"]
76+
},
77+
{
78+
"header": "UL1 Secondary rated voltage",
79+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[lnClass=\"TVTR\"]",
80+
"dataAttributePath": ["VRtgScy", "setMag", "f"]
81+
},
82+
{
83+
"header": "UL1 Devision ratio",
84+
"selector": "AccessPoint > Server > LDevice[inst=\"LD0\"] > LN[lnClass=\"TVTR\"]",
85+
"dataAttributePath": ["Rat", "setMag", "f"]
86+
},
87+
{
88+
"header": "Vendor",
89+
"selector": "AccessPoint > Server > LDevice > LN[lnClass=\"LPHD\"]",
90+
"dataAttributePath": ["PhyNam", "vendor"]
91+
},
92+
{
93+
"header": "Model",
94+
"selector": "AccessPoint > Server > LDevice > LN[lnClass=\"LPHD\"]",
95+
"dataAttributePath": ["PhyNam", "model"]
96+
}
97+
]
98+
}

public/js/plugins.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const officialPlugins = [
101101
default: true,
102102
kind: 'menu',
103103
requireDoc: false,
104-
position: 'top'
104+
position: 'top',
105105
},
106106
{
107107
name: 'Import from API',
@@ -110,7 +110,7 @@ export const officialPlugins = [
110110
default: false,
111111
kind: 'menu',
112112
requireDoc: false,
113-
position: 'top'
113+
position: 'top',
114114
},
115115
{
116116
name: 'Save project',
@@ -119,7 +119,7 @@ export const officialPlugins = [
119119
default: true,
120120
kind: 'menu',
121121
requireDoc: true,
122-
position: 'top'
122+
position: 'top',
123123
},
124124
{
125125
name: 'Save project as',
@@ -227,7 +227,16 @@ export const officialPlugins = [
227227
default: true,
228228
kind: 'menu',
229229
requireDoc: true,
230-
position: 'middle'
230+
position: 'middle',
231+
},
232+
{
233+
name: 'Export IED Params',
234+
src: '/src/menu/ExportIEDParams.js',
235+
icon: 'download',
236+
default: false,
237+
kind: 'menu',
238+
requireDoc: true,
239+
position: 'middle',
231240
},
232241
{
233242
name: 'Locamation VMU',
@@ -236,7 +245,7 @@ export const officialPlugins = [
236245
default: false,
237246
kind: 'menu',
238247
requireDoc: true,
239-
position: 'middle'
248+
position: 'middle',
240249
},
241250
{
242251
name: 'CoMPAS Settings',
@@ -245,7 +254,7 @@ export const officialPlugins = [
245254
default: true,
246255
kind: 'menu',
247256
requireDoc: false,
248-
position: 'bottom'
257+
position: 'bottom',
249258
},
250259
{
251260
name: 'Help',

src/menu/ExportIEDParams.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { LitElement, property } from 'lit-element';
2+
3+
import { stringify } from 'csv-stringify/browser/esm/sync';
4+
5+
import { compareNames } from '../foundation.js';
6+
7+
import { stripExtensionFromName } from '../compas/foundation.js';
8+
9+
import settings from '../../public/conf/export-ied-parameters.json';
10+
11+
function getDataElement(typeElement: Element, name: string): Element | null {
12+
if (typeElement.tagName === 'LNodeType') {
13+
return typeElement.querySelector(`:scope > DO[name="${name}"]`);
14+
} else if (typeElement.tagName === 'DOType') {
15+
return typeElement.querySelector(
16+
`:scope > SDO[name="${name}"], :scope > DA[name="${name}"]`
17+
);
18+
} else {
19+
return typeElement.querySelector(`:scope > BDA[name="${name}"]`);
20+
}
21+
}
22+
23+
function getValue(element: Element, attributeName: string | undefined): string {
24+
if (attributeName) {
25+
return element.getAttribute(attributeName) ?? '';
26+
}
27+
return element.textContent ?? '';
28+
}
29+
30+
function getSelector(selector: string, iedName: string) {
31+
return selector.replace('{{ iedName }}', iedName);
32+
}
33+
34+
function getElements(
35+
iedElement: Element,
36+
selector: string | undefined,
37+
useOwnerDocument: boolean
38+
): Element[] {
39+
let elements: Element[] = [iedElement];
40+
if (selector) {
41+
const iedName = iedElement.getAttribute('name') ?? '';
42+
const substitutedSelector = getSelector(selector, iedName);
43+
if (useOwnerDocument) {
44+
elements = Array.from(
45+
iedElement.ownerDocument.querySelectorAll(substitutedSelector)
46+
);
47+
} else {
48+
elements = Array.from(iedElement.querySelectorAll(substitutedSelector));
49+
}
50+
}
51+
return elements;
52+
}
53+
54+
export default class ExportIEDParametersPlugin extends LitElement {
55+
@property() doc!: XMLDocument;
56+
@property() docName!: string;
57+
58+
private getTypeElement(lastElement: Element | null): Element | null {
59+
if (lastElement) {
60+
if (['DO', 'SDO'].includes(lastElement.tagName)) {
61+
const type = lastElement.getAttribute('type') ?? '';
62+
return this.doc.querySelector(`DOType[id="${type}"]`);
63+
} else {
64+
const bType = lastElement.getAttribute('bType') ?? '';
65+
if (bType === 'Struct') {
66+
const type = lastElement.getAttribute('type') ?? '';
67+
return this.doc.querySelector(`DAType[id="${type}"]`);
68+
}
69+
}
70+
}
71+
return null;
72+
}
73+
74+
private getDataAttributeTemplateValue(
75+
element: Element,
76+
dataAttributePath: string[]
77+
): string | null {
78+
// This is only useful if the element to start from is the LN(0) Element.
79+
if (['LN', 'LN0'].includes(element.tagName)) {
80+
// Search LNodeType Element that is linked to the LN(0) Element.
81+
const type = element.getAttribute('lnType');
82+
let typeElement = this.doc.querySelector(`LNodeType[id="${type}"]`);
83+
let lastElement: Element | null = null;
84+
85+
// Now start search through the Template section jumping between the type elements.
86+
dataAttributePath.forEach(name => {
87+
if (typeElement) {
88+
lastElement = getDataElement(typeElement, name);
89+
typeElement = this.getTypeElement(lastElement);
90+
}
91+
});
92+
93+
if (lastElement) {
94+
const valElement = (<Element>lastElement).querySelector('Val');
95+
return valElement?.textContent ?? null;
96+
}
97+
}
98+
return null;
99+
}
100+
101+
private getDataAttributeInstanceValue(
102+
element: Element,
103+
dataAttributePath: string[]
104+
): string | null {
105+
const daiSelector = dataAttributePath
106+
.slice()
107+
.reverse()
108+
.map((path, index) => {
109+
if (index === 0) {
110+
return `DAI[name="${path}"]`;
111+
} else if (index === dataAttributePath.length - 1) {
112+
return `DOI[name="${path}"]`;
113+
}
114+
return `SDI[name="${path}"]`;
115+
})
116+
.reverse()
117+
.join(' > ');
118+
119+
const daiValueElement = element.querySelector(daiSelector + ' Val');
120+
if (daiValueElement) {
121+
return daiValueElement.textContent;
122+
}
123+
return null;
124+
}
125+
126+
private getDataAttributeValue(
127+
element: Element,
128+
dataAttributePath: string[]
129+
): string {
130+
let value = this.getDataAttributeInstanceValue(element, dataAttributePath);
131+
if (!value) {
132+
value = this.getDataAttributeTemplateValue(element, dataAttributePath);
133+
}
134+
return value ?? '';
135+
}
136+
137+
private content(): string[][] {
138+
return Array.from(this.doc.querySelectorAll(`IED`))
139+
.sort(compareNames)
140+
.map(iedElement => {
141+
return settings.columns.map(value => {
142+
const elements = getElements(
143+
iedElement,
144+
value.selector,
145+
value.useOwnerDocument ?? false
146+
);
147+
148+
return elements
149+
.map(element => {
150+
if (value.dataAttributePath) {
151+
return this.getDataAttributeValue(
152+
element,
153+
value.dataAttributePath
154+
);
155+
}
156+
return getValue(element, value.attributeName);
157+
})
158+
.filter(value => value!)
159+
.join(' / ');
160+
});
161+
});
162+
}
163+
164+
private columnHeaders(): string[] {
165+
return settings.columns.map(value => value.header);
166+
}
167+
168+
async run(): Promise<void> {
169+
const content = stringify(this.content(), {
170+
header: true,
171+
columns: this.columnHeaders(),
172+
});
173+
const blob = new Blob([content], {
174+
type: 'text/csv',
175+
});
176+
177+
const a = document.createElement('a');
178+
a.download = stripExtensionFromName(this.docName) + '-ied-parameters.csv';
179+
a.href = URL.createObjectURL(blob);
180+
a.dataset.downloadurl = ['text/csv', a.download, a.href].join(':');
181+
a.style.display = 'none';
182+
document.body.appendChild(a);
183+
a.click();
184+
document.body.removeChild(a);
185+
setTimeout(function () {
186+
URL.revokeObjectURL(a.href);
187+
}, 5000);
188+
}
189+
}

0 commit comments

Comments
 (0)