Skip to content

Commit 12c9123

Browse files
feat(UpdateDescriptionSEL): add menu type plugin for SEL specific IEDs (openscd#424)
* feat(menu/UpdateDescriptionSEL): add plug-in menu type plugin * feat(menu/UpdateDescriptionSEL): add unit tests * refactor(menu/UpdateDescriptionSEL): add better csv parser * test(menu/UpdateDescriptionSEL): update open-scd snapshot
1 parent 64a27d5 commit 12c9123

File tree

10 files changed

+837
-0
lines changed

10 files changed

+837
-0
lines changed

public/js/plugins.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ export const officialPlugins = [
100100
requireDoc: true,
101101
position: 'middle'
102102
},
103+
{
104+
name: 'Update desc (SEL)',
105+
src: '/src/menu/UpdateDescriptionSEL.js',
106+
default: false,
107+
kind: 'menu',
108+
requireDoc: true,
109+
position: 'middle'
110+
},
103111
{
104112
name: 'Merge Project',
105113
src: '/src/menu/Merge.js',

src/menu/UpdateDescriptionSEL.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { css, html, LitElement, query, TemplateResult } from 'lit-element';
2+
import { get } from 'lit-translate';
3+
4+
import '@material/mwc-list/mwc-check-list-item';
5+
import { List } from '@material/mwc-list';
6+
import { ListItemBase } from '@material/mwc-list/mwc-list-item-base';
7+
8+
import '../filtered-list.js';
9+
import {
10+
cloneElement,
11+
identity,
12+
isPublic,
13+
newWizardEvent,
14+
SCLTag,
15+
selector,
16+
Wizard,
17+
WizardAction,
18+
WizardActor,
19+
WizardInput,
20+
} from '../foundation.js';
21+
22+
interface SignalDescription {
23+
desc: string;
24+
tag: SCLTag;
25+
identity: string | number;
26+
}
27+
28+
function addDescriptionToSEL(
29+
ied: Element,
30+
signalList: string[][]
31+
): SignalDescription[] {
32+
const iedName = ied.getAttribute('name');
33+
const manufacturer = ied.getAttribute('manufacturer');
34+
if (!iedName || manufacturer !== 'SEL') return [];
35+
36+
return <SignalDescription[]>Array.from(ied.getElementsByTagName('DAI'))
37+
.filter(element => isPublic(element))
38+
.filter(dai => {
39+
const datasrc = dai.getAttributeNS(
40+
'http://www.selinc.com/2006/61850',
41+
'datasrc'
42+
);
43+
return datasrc?.startsWith('db:');
44+
})
45+
.map(dai => {
46+
//the next lines are vendor dependant!!
47+
const datasrc = dai.getAttributeNS(
48+
'http://www.selinc.com/2006/61850',
49+
'datasrc'
50+
);
51+
52+
const tag = datasrc ? datasrc.replace('db:', '') : null;
53+
const desc =
54+
signalList.find(row => row[2] === tag && row[1] === iedName)?.[0] ??
55+
null;
56+
57+
return desc ? { desc, tag: 'DAI', identity: identity(dai) } : null;
58+
})
59+
.filter(signalDescription => signalDescription);
60+
}
61+
62+
function addDescriptionAction(doc: XMLDocument): WizardActor {
63+
return (
64+
_: WizardInput[],
65+
wizard: Element,
66+
list: List | null | undefined
67+
): WizardAction[] => {
68+
const selectedItems = <ListItemBase[]>list!.selected;
69+
70+
const actions = selectedItems.map(item => {
71+
const desc = (<Element>item.querySelector('span')).textContent;
72+
const [tag, identity] = item.value.split(' | ');
73+
74+
const oldElement = doc.querySelector(selector(tag, identity))!;
75+
const newElement = cloneElement(oldElement, { desc });
76+
return { old: { element: oldElement }, new: { element: newElement } };
77+
});
78+
79+
return [
80+
{
81+
title: get('updatedesc.sel'),
82+
actions,
83+
},
84+
];
85+
};
86+
}
87+
88+
function createLogWizard(doc: XMLDocument, items: SignalDescription[]): Wizard {
89+
return [
90+
{
91+
title: get('wizard.title.add', { tagName: 'desc' }),
92+
primary: {
93+
label: get('save'),
94+
icon: 'save',
95+
action: addDescriptionAction(doc),
96+
},
97+
content: [
98+
html`<filtered-list multi
99+
>${Array.from(
100+
items.map(
101+
item =>
102+
html`<mwc-check-list-item
103+
twoline
104+
selected
105+
value="${item.tag + ' | ' + item.identity}"
106+
><span>${item.desc}</span
107+
><span slot="secondary"
108+
>${item.tag + ' | ' + item.identity}</span
109+
></mwc-check-list-item
110+
>`
111+
)
112+
)}</filtered-list
113+
>`,
114+
],
115+
},
116+
];
117+
}
118+
119+
function parseCsv(str: string, delimiter: ',' | ';'): string[][] {
120+
// predefined for later use
121+
const quoteChar = '"',
122+
escapeChar = '\\';
123+
124+
const entries: string[][] = [];
125+
let isInsideQuote = false;
126+
127+
// Iterate over each character, keep track of current row and column (of the returned array)
128+
for (let row = 0, col = 0, char = 0; char < str.length; char++) {
129+
const currentChar = str[char];
130+
const nextChar = str[char + 1];
131+
132+
entries[row] = entries[row] || [];
133+
entries[row][col] = entries[row][col] || '';
134+
135+
//Ignore escape character
136+
if (currentChar === escapeChar) {
137+
entries[row][col] += nextChar;
138+
++char;
139+
continue;
140+
}
141+
142+
// Check for quoted characters. Do not miss-interpret delimiter within field
143+
if (currentChar === quoteChar) {
144+
isInsideQuote = !isInsideQuote;
145+
continue;
146+
}
147+
148+
if (!isInsideQuote) {
149+
if (currentChar === delimiter) {
150+
++col;
151+
entries[row][col] = '';
152+
continue;
153+
}
154+
155+
if (currentChar === '\n' || currentChar === '\r') {
156+
++row;
157+
col = 0;
158+
159+
// Skip the next character for CRLF
160+
if (currentChar === '\r' && nextChar === '\n') ++char;
161+
162+
continue;
163+
}
164+
}
165+
166+
entries[row][col] += currentChar;
167+
}
168+
169+
return entries;
170+
}
171+
172+
function getGuessDelimiter(csvString: string): ';' | ',' {
173+
let numberComma = 0,
174+
numberSemicolon = 0;
175+
176+
const quoteChar = '"';
177+
178+
let isInsideQuote = false;
179+
for (const currentChar of csvString) {
180+
// Check for quoted characters. Do not miss-interpret delimiter within field
181+
if (currentChar === quoteChar) {
182+
isInsideQuote = !isInsideQuote;
183+
continue;
184+
}
185+
186+
if (!isInsideQuote) {
187+
if (currentChar === ';') {
188+
numberSemicolon++;
189+
continue;
190+
}
191+
192+
if (currentChar === ',') {
193+
numberComma++;
194+
continue;
195+
}
196+
}
197+
}
198+
199+
return numberComma > numberSemicolon ? ',' : ';';
200+
}
201+
202+
/**
203+
* Plug-in that enriches the desc attribute in SEL type IED elements based on a signal list
204+
* The signal list must be a ; or , separated CSV file with 3 columns.
205+
* 1st column: signal name
206+
* 2nd column: IED name
207+
* 3rd column: identifier from the SEL namespace excluding the prefix of "db:",
208+
* similar to relay word bit name (RWB), e.g. SV24T, 51P1T, IN203
209+
*/
210+
export default class UpdateDescriptionSel extends LitElement {
211+
/** The document being edited as provided to plugins by [[`OpenSCD`]]. */
212+
doc!: XMLDocument;
213+
214+
@query('#plugin-input') pluginFileUI!: HTMLInputElement;
215+
216+
processSignalList(csvString: string): void {
217+
const signalList = parseCsv(csvString, getGuessDelimiter(csvString));
218+
219+
const items = Array.from(this.doc.querySelectorAll('IED'))
220+
.filter(ied => isPublic(ied))
221+
.flatMap(ied => addDescriptionToSEL(ied, signalList));
222+
223+
document
224+
.querySelector('open-scd')
225+
?.dispatchEvent(newWizardEvent(createLogWizard(this.doc, items)));
226+
}
227+
228+
private async onFileInput(e: Event): Promise<void> {
229+
const file = (<HTMLInputElement | null>e.target)?.files?.item(0) ?? false;
230+
if (!file) return;
231+
232+
this.processSignalList(await file.text());
233+
}
234+
235+
/** Entry point for this plug-in */
236+
async run(): Promise<void> {
237+
this.pluginFileUI.click();
238+
}
239+
240+
render(): TemplateResult {
241+
return html`<input @click=${(event: MouseEvent) =>
242+
((<HTMLInputElement>event.target).value = '')} @change=${(e: Event) =>
243+
this.onFileInput(
244+
e
245+
)} id="plugin-input" accept=".csv" type="file"></input>`;
246+
}
247+
248+
static styles = css`
249+
input {
250+
width: 0;
251+
height: 0;
252+
opacity: 0;
253+
}
254+
`;
255+
}

src/translations/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ export const de: Translations = {
402402
},
403403
updatedesc: {
404404
abb: 'Signalbeschreibungen zu ABB IEDs hinzugefügt',
405+
sel: 'Signalbeschreibungen zu SEL IEDs hinzugefügt',
405406
},
406407
sld: {
407408
substationSelector: 'Schaltanlage auswählen',

src/translations/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ export const en = {
399399
},
400400
updatedesc: {
401401
abb: 'Added signal descriptions to ABB IEDs',
402+
sel: 'Added signal descriptions to SEL IEDs',
402403
},
403404
sld: {
404405
substationSelector: 'Select a substation',

test/integration/__snapshots__/open-scd.test.snap.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,21 @@ snapshots["open-scd looks like its snapshot"] =
798798
</mwc-icon>
799799
Update desc (ABB)
800800
</mwc-check-list-item>
801+
<mwc-check-list-item
802+
aria-disabled="false"
803+
class="official"
804+
graphic="control"
805+
hasmeta=""
806+
left=""
807+
mwc-list-item=""
808+
tabindex="-1"
809+
value="/src/menu/UpdateDescriptionSEL.js"
810+
>
811+
<mwc-icon slot="meta">
812+
play_circle
813+
</mwc-icon>
814+
Update desc (SEL)
815+
</mwc-check-list-item>
801816
<mwc-check-list-item
802817
aria-disabled="false"
803818
class="official"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
CB_CLOSED,IED2,IN101
3+
CB_OPEN,IED2,IN102
4+
BB1_DS_CLOSED,IED2,IN103
5+
BB1_DS_OPEN,IED2,IN104
6+
BB2_DS_CLOSED,IED2,IN105
7+
BB2_DS_OPEN,IED2,IN106
8+
LINE_DS_CLOSED,IED2,IN107
9+
LINE_DS_CLOSED,IED2
10+
LINE_DS_CLOSED
11+
LINE_DS_CLOSED,IED2,"IN,107"
12+
,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CB_CLOSED;IED2;IN101
2+
CB_OPEN;IED2;IN102
3+
BB1_DS_CLOSED;IED2;IN103
4+
BB1_DS_OPEN;IED2;IN104
5+
BB2_DS_CLOSED;IED2;IN105
6+
BB2_DS_OPEN;IED2;IN106
7+
LINE_DS_CLOSED;IED2;"IN107"
8+
LINE_DS_CLOSED;"IED2"
9+
LINE_DS_CLOSED
10+
;

0 commit comments

Comments
 (0)