Skip to content

Commit 8fab275

Browse files
committed
feat: Add 104 export menu plugin
1 parent 4e0405e commit 8fab275

File tree

3 files changed

+217
-0
lines changed

3 files changed

+217
-0
lines changed

packages/compas-open-scd/public/js/plugins.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,13 @@ export const officialPlugins = [
336336
requireDoc: true,
337337
position: 'middle',
338338
},
339+
{
340+
name: 'Export IEC 104 CSV',
341+
src: '/plugins/src/menu/Export104.js',
342+
icon: 'sim_card_download',
343+
default: false,
344+
kind: 'menu',
345+
requireDoc: true,
346+
position: 'middle',
347+
},
339348
];
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { LitElement, property } from 'lit-element';
2+
import { stringify } from 'csv-stringify/browser/esm/sync';
3+
4+
import { extractAllSignal104Data, Signal104 } from './export104/foundation.js';
5+
6+
7+
8+
export default class Export104 extends LitElement {
9+
@property({ attribute: false }) doc!: XMLDocument;
10+
@property() docName!: string;
11+
12+
private readonly csvHeaders = [
13+
'Id',
14+
'Name',
15+
'Signal Number',
16+
'mIOA',
17+
'cIOA'
18+
];
19+
20+
async run(): Promise<void> {
21+
22+
const allSignal104Data = extractAllSignal104Data(this.doc);
23+
const csvLines = this.generateCsvLines(allSignal104Data);
24+
25+
const csvContent = stringify(csvLines, {
26+
header: true,
27+
columns: this.csvHeaders,
28+
});
29+
const csvBlob = new Blob([csvContent], {
30+
type: 'text/csv',
31+
});
32+
33+
this.downloadCsv(csvBlob);
34+
35+
console.log(csvLines);
36+
37+
console.log('Export104', allSignal104Data);
38+
}
39+
40+
private generateCsvLines(allSignal104Data: Signal104[]): string[][] {
41+
const lines: string[][] = [];
42+
43+
for(const signal104Data of allSignal104Data) {
44+
const line = [
45+
'',
46+
signal104Data.name ?? '',
47+
signal104Data.signalNumber ?? '',
48+
];
49+
50+
if (signal104Data.isMonitorSignal) {
51+
line.push(signal104Data.ioa ?? '', '');
52+
} else {
53+
line.push('', signal104Data.ioa ?? '');
54+
}
55+
56+
lines.push(line);
57+
}
58+
59+
return lines;
60+
}
61+
62+
private downloadCsv(csvBlob: Blob): void {
63+
const a = document.createElement('a');
64+
a.download = this.docName + '-104-signals.csv';
65+
a.href = URL.createObjectURL(csvBlob);
66+
a.dataset.downloadurl = ['text/csv', a.download, a.href].join(':');
67+
a.style.display = 'none';
68+
69+
document.body.appendChild(a);
70+
a.click();
71+
document.body.removeChild(a);
72+
URL.revokeObjectURL(a.href);
73+
}
74+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// TODO: Share with 104 editor plugin?
2+
export const PROTOCOL_104_PRIVATE = 'IEC_60870_5_104';
3+
4+
export interface Signal104 {
5+
name: string | null;
6+
signalNumber: string | null;
7+
isMonitorSignal: boolean;
8+
ioa: string | null;
9+
ti: string | null;
10+
}
11+
12+
enum SignalType {
13+
Monitor,
14+
Control,
15+
Unknown
16+
}
17+
18+
const private104Selector = `Private[type="${PROTOCOL_104_PRIVATE}"]`;
19+
20+
export function extractAllSignal104Data(doc: XMLDocument): Signal104[] {
21+
const allSignal104Data: Signal104[] = [];
22+
const address104Elements = doc.querySelectorAll(`${private104Selector} > Address`);
23+
24+
address104Elements.forEach((addressElement) => {
25+
const signal104Data = extractSignal104Data(addressElement, doc);
26+
27+
if (signal104Data) {
28+
allSignal104Data.push(signal104Data);
29+
}
30+
});
31+
32+
console.log(address104Elements);
33+
34+
return allSignal104Data;
35+
}
36+
37+
function extractSignal104Data(addressElement: Element, doc: XMLDocument): Signal104 | null {
38+
const ti = addressElement.getAttribute('ti');
39+
const ioa = addressElement.getAttribute('ioa');
40+
41+
// By convention the last four digits of the ioa are the signalnumber, see https://github.com/com-pas/compas-open-scd/issues/334
42+
if (ti === null || ioa === null || ioa.length < 4) {
43+
console.log('No ti, io or io too short');
44+
return null;
45+
}
46+
const signalNumber = ioa.slice(-4);
47+
48+
const signalType = getSignalType(ti);
49+
if (signalType === SignalType.Unknown) {
50+
console.log('Unknown signal type');
51+
return null;
52+
}
53+
const isMonitorSignal = signalType === SignalType.Monitor;
54+
55+
// TODO: Doi desc, Bay name, VoltageLevel name, Substation name
56+
57+
addressElement.parentElement;
58+
const parentDOI = addressElement.closest('DOI');
59+
60+
if (!parentDOI) {
61+
console.log('No parent DOI');
62+
return null;
63+
}
64+
65+
const doiDesc = parentDOI.getAttribute('desc');
66+
const parentIED = parentDOI.closest('IED');
67+
if (!parentIED) {
68+
console.log('No parent IED');
69+
return null;
70+
}
71+
72+
const iedName = parentIED.getAttribute('name');
73+
74+
const lNodeQuery = `Substation > VoltageLevel > Bay LNode[iedName="${iedName}"]`;
75+
console.log(lNodeQuery);
76+
const parentLNode = doc.querySelector(lNodeQuery);
77+
78+
if (!parentLNode) {
79+
console.log('No parent LNode');
80+
return null;
81+
}
82+
83+
const parentBay = parentLNode.closest('Bay');
84+
85+
if (!parentBay) {
86+
console.log('No parent Bay');
87+
return null;
88+
}
89+
90+
const bayName = parentBay.getAttribute('name');
91+
const parentVoltageLevel = parentBay.closest('VoltageLevel');
92+
93+
if (!parentVoltageLevel) {
94+
console.log('No parent VL');
95+
return null;
96+
}
97+
98+
const voltageLevelName = parentVoltageLevel.getAttribute('name');
99+
const parentSubstation = parentVoltageLevel.closest('Substation');
100+
101+
if (!parentSubstation) {
102+
console.log('No parent Substation');
103+
return null;
104+
}
105+
106+
const substationName = parentSubstation.getAttribute('name');
107+
108+
const name = `${substationName}${voltageLevelName}${bayName}${doiDesc}`;
109+
110+
return {
111+
name,
112+
signalNumber,
113+
isMonitorSignal,
114+
ti,
115+
ioa
116+
}
117+
}
118+
119+
// For signal classification details see https://github.com/com-pas/compas-open-scd/issues/334
120+
function getSignalType(tiString: string): SignalType {
121+
const ti = parseInt(tiString);
122+
123+
if (isNaN(ti)) {
124+
return SignalType.Unknown;
125+
}
126+
127+
if ((ti >= 1 && ti <= 21) || (ti >= 30 && ti <= 40)) {
128+
return SignalType.Monitor;
129+
} else if ((ti >= 45 && ti <= 51) || (ti >= 58 && ti <= 64)) {
130+
return SignalType.Control;
131+
} else {
132+
return SignalType.Unknown;
133+
}
134+
}

0 commit comments

Comments
 (0)