Skip to content

Commit 45b0a6f

Browse files
authored
Merge pull request #231 from com-pas/227-add-substation-section-based-on-ied-name-convention
227 add substation section based on ied name convention
2 parents 385f31c + 16706bb commit 45b0a6f

File tree

5 files changed

+71723
-3
lines changed

5 files changed

+71723
-3
lines changed

public/js/plugins.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,13 @@ export const officialPlugins = [
327327
default: true,
328328
kind: 'editor',
329329
},
330+
{
331+
name: 'Autogen Substation',
332+
src: '/src/editors/substation/autogen-substation/autogen-substation.js',
333+
icon: 'playlist_add_circle',
334+
default: true,
335+
kind: 'menu',
336+
requireDoc: true,
337+
position: 'middle',
338+
},
330339
];
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
import { LitElement, property } from 'lit-element';
2+
import { createElement, newActionEvent } from '../../../foundation.js';
3+
4+
let cbNum = 1;
5+
let dsNum = 1;
6+
7+
//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard
8+
// Check on a later point if implementation of an export of this function will remain valid.
9+
function addLNodes(condEq: Element, cswi: Element): Element {
10+
// switchgear ideally is a composition of lnClass CILO,CSWI,XSWI
11+
cswi.parentElement
12+
?.querySelectorAll(
13+
`LN[lnClass="CSWI"]${
14+
cswi.getAttribute('prefix')
15+
? `[prefix="${cswi.getAttribute('prefix')}"]`
16+
: ``
17+
}${
18+
cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : ``
19+
},LN[lnClass="CILO"]${
20+
cswi.getAttribute('prefix')
21+
? `[prefix="${cswi.getAttribute('prefix')}"]`
22+
: ``
23+
}${
24+
cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : ``
25+
},LN[lnClass="XCBR"]${
26+
cswi.getAttribute('prefix')
27+
? `[prefix="${cswi.getAttribute('prefix')}"]`
28+
: ``
29+
}${
30+
cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : ``
31+
},LN[lnClass="XSWI"]${
32+
cswi.getAttribute('prefix')
33+
? `[prefix="${cswi.getAttribute('prefix')}"]`
34+
: ``
35+
}${
36+
cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : ``
37+
}`
38+
)
39+
.forEach(ln => {
40+
condEq.appendChild(
41+
createElement(cswi.ownerDocument, 'LNode', {
42+
iedName:
43+
ln.parentElement?.parentElement?.parentElement?.parentElement?.getAttribute(
44+
'name'
45+
) ?? null,
46+
ldInst: cswi.parentElement?.getAttribute('inst') ?? null,
47+
prefix: ln.getAttribute('prefix'),
48+
lnClass: ln.getAttribute('lnClass'),
49+
lnInst: ln.getAttribute('inst'),
50+
})
51+
);
52+
});
53+
54+
return condEq;
55+
}
56+
57+
//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard
58+
// Check on a later point if implementation of an export of this function will remain valid.
59+
function getSwitchGearType(cswi: Element): string {
60+
return cswi.parentElement?.querySelector(
61+
`LN[lnClass="XCBR"]${
62+
cswi.getAttribute('prefix')
63+
? `[prefix="${cswi.getAttribute('prefix')}"]`
64+
: ``
65+
}${
66+
cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : ``
67+
}`
68+
)
69+
? 'CBR'
70+
: 'DIS';
71+
}
72+
73+
//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard
74+
// Check on a later point if implementation of an export of this function will remain valid.
75+
function getSwitchGearName(ln: Element): string {
76+
if (ln.getAttribute('prefix') && ln.getAttribute('inst'))
77+
return ln.getAttribute('prefix')! + ln.getAttribute('inst');
78+
79+
if (ln.getAttribute('inst') && getSwitchGearType(ln) === 'CBR')
80+
return 'QA' + cbNum++;
81+
82+
return 'QB' + dsNum++;
83+
}
84+
85+
//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard
86+
// Check on a later point if implementation of an export of this function will remain valid.
87+
function isSwitchGear(ln: Element, selectedCtlModel: string[]): boolean {
88+
// ctlModel can be configured in IED section.
89+
if (
90+
Array.from(
91+
ln.querySelectorAll('DOI[name="Pos"] > DAI[name="ctlModel"] > Val')
92+
).filter(val => selectedCtlModel.includes(val.innerHTML.trim())).length
93+
)
94+
return true;
95+
96+
// ctlModel can be configured as type in DataTypeTemplate section
97+
const doc = ln.ownerDocument;
98+
return (
99+
Array.from(
100+
doc.querySelectorAll(
101+
`DataTypeTemplates > LNodeType[id="${ln.getAttribute(
102+
'lnType'
103+
)}"] > DO[name="Pos"]`
104+
)
105+
)
106+
.map(DO => (<Element>DO).getAttribute('type'))
107+
.flatMap(doType =>
108+
Array.from(
109+
doc.querySelectorAll(
110+
`DOType[id="${doType}"] > DA[name="ctlModel"] > Val`
111+
)
112+
)
113+
)
114+
.filter(val => selectedCtlModel.includes((<Element>val).innerHTML.trim()))
115+
.length > 0
116+
);
117+
}
118+
119+
//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard
120+
// Check on a later point if implementation of an export of this function will remain valid.
121+
function getCSWI(ied: Element): Element[] {
122+
return Array.from(
123+
ied.querySelectorAll('AccessPoint > Server > LDevice > LN[lnClass="CSWI"]')
124+
);
125+
}
126+
127+
//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard
128+
// Check on a later point if implementation of an export of this function will remain valid.
129+
function getValidCSWI(ied: Element, selectedCtlModel: string[]): Element[] {
130+
if (!ied.parentElement) return [];
131+
132+
return getCSWI(ied).filter(cswi => isSwitchGear(cswi, selectedCtlModel));
133+
}
134+
135+
export default class CompasAutogenerateSubstation extends LitElement {
136+
@property() doc!: XMLDocument;
137+
@property() iedNames!: string[];
138+
@property() substationSeperator = '__';
139+
@property() voltageLevelNameLength = 3;
140+
@property() iedStartChar = 'A';
141+
142+
async run(): Promise<void> {
143+
//Get the lNodes inside the document that are already linked to and IED
144+
const lNodes = this.extractNames(
145+
Array.from(this.doc.querySelectorAll('LNode')).map(
146+
value => value.getAttribute('iedName') || ''
147+
)
148+
);
149+
150+
//Get name attribute from all the IEDs that aren't linked in the document
151+
this.iedNames = Array.from(this.doc.querySelectorAll('IED'))
152+
.map(IED => IED.getAttribute('name') || '')
153+
.filter(value => !lNodes.includes(value));
154+
155+
//Get all the substation names by splitting the names on the '__' (seperator) and getting the characters in front of it
156+
const substationNames = this.extractNames(
157+
this.iedNames.map(FullName =>
158+
FullName.includes(this.substationSeperator)
159+
? FullName?.split(this.substationSeperator)[0]
160+
: ''
161+
)
162+
);
163+
this.createSubstations(substationNames);
164+
}
165+
166+
/**
167+
* Creating substations based on the list of names. First check if a substation with that name already exists.
168+
* If the substation element doesn't exist yet, a substation element will be created with the given name and a default
169+
* description.
170+
*
171+
* The created substation element with its name will be used to create voltageLevels as child elements to the substations.
172+
* Afterwards the substation elements will be added to the document.
173+
*/
174+
createSubstations(substationNames: string[]) {
175+
substationNames.forEach(async name => {
176+
if (this.doc.querySelector(`Substation[name=${name}]`) === null) {
177+
const desc = 'Substation generated by CoMPAS';
178+
const substation = createElement(this.doc, 'Substation', {
179+
name,
180+
desc,
181+
});
182+
183+
await this.createVoltageLevels(substation, name);
184+
185+
this.dispatchEvent(
186+
newActionEvent({
187+
new: {
188+
parent: this.doc.querySelector('SCL')!,
189+
element: substation,
190+
},
191+
})
192+
);
193+
}
194+
});
195+
}
196+
197+
/**
198+
* The name-content of the child elements will be extracted by splitting the ied name on the substationSeperator ('__' by default)
199+
* character and getting the characters after it.
200+
* VoltageLevel elements will be created by getting the first voltageLevelNameLength characters of each element in the name content.
201+
* The elements will be created based on the name and some default values.
202+
*
203+
* Afterwards the first voltageLevelNameLength of characters will be filtered out of the name content and the remaining content
204+
* will be used to create bay elements.
205+
* The elements will be appended to the substation element
206+
* @param substation substation(parent) element
207+
* @param substationName name of the substation
208+
*/
209+
createVoltageLevels(substation: Element, substationName: string) {
210+
const substationContent = this.iedNames
211+
.filter(value => value.includes(substationName))
212+
.map(FullName => FullName?.split(this.substationSeperator)[1]);
213+
214+
const voltageLevelNames = this.extractNames(
215+
substationContent.map(FullName =>
216+
FullName?.substring(0, this.voltageLevelNameLength)
217+
)
218+
);
219+
220+
voltageLevelNames.forEach(name => {
221+
const desc = 'Voltage Level generated by CoMPAS';
222+
const nomFreq = '50.0';
223+
const numPhases = '3';
224+
const voltageLevel = createElement(
225+
substation.ownerDocument,
226+
'VoltageLevel',
227+
{
228+
name,
229+
desc,
230+
nomFreq,
231+
numPhases,
232+
}
233+
);
234+
235+
const voltageLevelContent = substationContent
236+
.filter(value => value.startsWith(name))
237+
.map(FullName =>
238+
FullName?.substring(this.voltageLevelNameLength, FullName.length)
239+
);
240+
241+
this.createBays(
242+
voltageLevel,
243+
voltageLevelContent,
244+
substationName + this.substationSeperator + name
245+
);
246+
substation.appendChild(voltageLevel);
247+
});
248+
}
249+
250+
/**
251+
* Bay elements will be created by getting the characters before the IED start character of each element in the remaining name content.
252+
* Afterwards the IED names that contain in the bay will be determined by filtering out the Voltage Level content that start with the bayname.
253+
* The IED element will be found by doing a query with the full IED name (substationVoltageLevelName + IED name).
254+
* The IED element will be used the create LNode elements. The guessLNodes will create elements based on if the IED contains switchgear.
255+
* If the element doesn't contain switchgear a deafult LNode element will be created as child and appended to the bay.
256+
*
257+
* @param voltageLevel voltageLevel(parent) element
258+
* @param voltageLevelContent remaining content extracted from the IEDs (name without substation name and voltageLevel name)
259+
* @param substationVoltageLevelName The name of the substation + voltageLevel
260+
*/
261+
createBays(
262+
voltageLevel: Element,
263+
voltageLevelContent: string[],
264+
substationVoltageLevelName: string
265+
) {
266+
const bayNames = this.extractNames(
267+
voltageLevelContent.map(iedName => iedName.split(this.iedStartChar)[0])
268+
);
269+
270+
bayNames.forEach(name => {
271+
const desc = 'Bay generated by CoMPAS';
272+
const bayElement = createElement(voltageLevel.ownerDocument, 'Bay', {
273+
name,
274+
desc,
275+
});
276+
277+
const iedNames = voltageLevelContent.filter(value =>
278+
value.startsWith(name)
279+
);
280+
iedNames.forEach(iedName => {
281+
const currentIed = this.doc.querySelector(
282+
`IED[name=${substationVoltageLevelName + iedName}]`
283+
);
284+
285+
const guessLNodes = this.createLNodeElements(currentIed!, [
286+
'sbo-with-enhanced-security',
287+
]);
288+
289+
guessLNodes
290+
? guessLNodes.forEach(lNode => bayElement.appendChild(lNode))
291+
: bayElement.prepend(this.addDefaultLNodes(currentIed!));
292+
});
293+
voltageLevel.appendChild(bayElement);
294+
});
295+
}
296+
297+
/**
298+
* Create ConductingEquipment with LNode children based on whether the IED contains switchgear
299+
* @param ied
300+
* @param ctlModelList
301+
* @returns ConductingEquipment with LNode child elements or null
302+
*/
303+
createLNodeElements(ied: Element, ctlModelList: string[]): Element[] | null {
304+
const switchGear = getValidCSWI(ied, ctlModelList);
305+
306+
if (switchGear.length) {
307+
const condEq = switchGear.map(cswi => {
308+
return addLNodes(
309+
createElement(ied.ownerDocument, 'ConductingEquipment', {
310+
name: getSwitchGearName(cswi),
311+
type: getSwitchGearType(cswi),
312+
}),
313+
cswi
314+
);
315+
});
316+
317+
return condEq;
318+
}
319+
return null;
320+
}
321+
322+
/**
323+
* Create default LNode element with default values
324+
* @param currentIed
325+
* @returns LNode element
326+
*/
327+
addDefaultLNodes(currentIed: Element): Element {
328+
const ln = currentIed.querySelector('LN0');
329+
330+
return createElement(currentIed.ownerDocument, 'LNode', {
331+
iedName: currentIed.getAttribute('name'),
332+
ldInst: currentIed.parentElement?.getAttribute('inst') ?? null,
333+
prefix: ln?.getAttribute('prefix') ?? null,
334+
lnClass: ln?.getAttribute('lnClass') ?? null,
335+
lnInst: ln?.getAttribute('inst') ?? null,
336+
});
337+
}
338+
339+
/**
340+
* Helper function to filter out empty and duplicate elements
341+
* @param content
342+
* @returns filtered content
343+
*/
344+
extractNames(content: string[]) {
345+
//Check for empty elements
346+
const elementNames = content.filter(Boolean);
347+
348+
//return list of names after filtering out the duplicate elements
349+
return elementNames.filter(
350+
(value, index) => value && elementNames.indexOf(<string>value) === index
351+
);
352+
}
353+
}

0 commit comments

Comments
 (0)