Skip to content

Commit 546419f

Browse files
feat(wizards/reportcontrol): add create wizard (openscd#544)
* refactor(wizards/trgops): improve code for better sharing * refactor(wizards/optfields): improve code for better sharing * feat(icons): add report control * feat(foundation): add function that gives unique names * refactor(wizards/fcda): move finder-list definition * add to optfields commit * add to trgops commit * feat(wizards/reportcontrol): add create wizard and location selector * refactor(wizards/reportcontrol): restrict parent selection to IED * refactor(wizards/reportcontrol): better naming * fix(wizards/reportcontrol): fix invalid parent handling * feat(wizards/reportcontrol): hide add report button with missing valid parent * test(wizards/reportcontrol): improve unit tests * test(wizards/reportcontrol): add integration tests * test: remove dead snapshot definitions * fix: merge conflicts * fix(wizards/reportcontrol): wrong rptID definition * fix(wizards/reportcontrol): configuration revision handling * fix(icons): remove unused icon
1 parent 9be83ab commit 546419f

21 files changed

+2111
-1138
lines changed

src/foundation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2587,6 +2587,18 @@ export function depth(t: Record<string, unknown>, mem = new WeakSet()): number {
25872587
}
25882588
}
25892589

2590+
export function getUniqueElementName(
2591+
parent: Element,
2592+
tagName: string,
2593+
iteration = 1
2594+
): string {
2595+
const newName = 'new' + tagName + iteration;
2596+
const child = parent.querySelector(`:scope > ${tagName}[name="${newName}"]`);
2597+
2598+
if (!child) return newName;
2599+
else return getUniqueElementName(parent, tagName, ++iteration);
2600+
}
2601+
25902602
export function findFCDAs(extRef: Element): Element[] {
25912603
if (extRef.tagName !== 'ExtRef' || extRef.closest('Private')) return [];
25922604

src/translations/de.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,34 @@ export const de: Translations = {
3030
dchg: 'Detenänderung ist Auslöser',
3131
qchg: 'Qualitätsanderung ist Auslöser',
3232
dupd: 'Datenupdate ist Auslöser',
33+
period: 'Periodisch übertragen',
34+
gi: 'Manuelle Abfrage',
3335
fixedOffs: 'Fester Offset',
3436
securityEnable: 'Aktive Sicherungsmaßnahmen',
35-
DataSet: 'Datensetz',
37+
DataSet: 'Datensatz',
3638
Communication: 'Kommunikation',
3739
TrgOps: 'Triggerbedingungen',
3840
OptFields: 'Optionale felder',
3941
multicast: 'SMV nach IEC 61850 9-2',
4042
smpMod: 'Abtast-Art',
4143
smpRate: 'Abtastrate',
4244
nofASDU: 'Abtastpunkte pro Datenpacket',
45+
seqNum: 'Sequenznummer mitschicken',
46+
timeStamp: 'Zeitstempel mitschicken',
47+
dataSet: 'Datensatz-Reference mitschicken',
48+
reasonCode: 'Was hat den Report getriggert?',
49+
dataRef: 'Beschreibung der Datensatzes',
50+
entryID: 'Entry ID mitschicken',
51+
configRef: 'Konfigurations-Revision mitschicken',
52+
bufOvfl: 'Überlauf des internen Speichers signalisieren',
53+
indexed: 'Mehrere Instanzen möglich',
54+
buffered: 'Gepufferter Report',
55+
maxReport: 'Anzahl Instanzen',
56+
bufTime: 'Min. Intervall zwischen zwei Reports',
57+
intgPd: 'Intervall zwischen zwei periodischen Reports',
4358
SmvOpts: 'Optionale Informationen',
4459
refreshTime: 'Zeitstempel des Abtastwertes zu Telegram hinzufügen',
4560
sampleRate: 'Abtastrate zu Telegram hinzufügen',
46-
dataSet: 'Datensatznamen zu Telegram hinzufügen',
4761
security: 'Potentiel in Zukunft für z.B. digitale Signature',
4862
synchSourceId: 'Identität der Zeitquelle zu Telegram hinzufügen',
4963
},
@@ -56,8 +70,10 @@ export const de: Translations = {
5670
showieds: 'Zeige IEDs im Substation-Editor',
5771
selectFileButton: 'Datei auswählen',
5872
loadNsdTranslations: 'NSDoc-Dateien hochladen',
59-
invalidFileNoIdFound: 'Ungültiges NSDoc; kein \'id\'-Attribut in der Datei gefunden',
60-
invalidNsdocVersion: 'Die Version {{ id }} NSD ({{ nsdVersion }}) passt nicht zu der geladenen NSDoc ({{ nsdocVersion }})'
73+
invalidFileNoIdFound:
74+
"Ungültiges NSDoc; kein 'id'-Attribut in der Datei gefunden",
75+
invalidNsdocVersion:
76+
'Die Version {{ id }} NSD ({{ nsdVersion }}) passt nicht zu der geladenen NSDoc ({{ nsdocVersion }})',
6177
},
6278
menu: {
6379
new: 'Neues projekt',
@@ -448,6 +464,13 @@ export const de: Translations = {
448464
yCoordinateHelper: 'Y-Koordinate im Einphasenersatzschaltbild',
449465
},
450466
},
467+
dataset: {
468+
fcda: { add: 'Daten-Attribute hinzufügen' },
469+
fcd: { add: 'Daten-Objekte hinzufügen' },
470+
},
471+
report: {
472+
wizard: { location: 'Ablageort der Reports wählen' },
473+
},
451474
add: 'Hinzufügen',
452475
new: 'Neu',
453476
remove: 'Entfernen',

src/translations/en.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const en = {
2828
dchg: 'Trigger on data change',
2929
qchg: 'Trigger on quality change',
3030
dupd: 'Trigger on data update',
31+
period: 'Periodical Publishing',
32+
gi: 'General Interrogation',
3133
fixedOffs: 'Fixed offset',
3234
securityEnable: 'Security enabled',
3335
DataSet: 'Dataset',
@@ -38,10 +40,22 @@ export const en = {
3840
smpMod: 'Sample mode',
3941
smpRate: 'Sample rate',
4042
nofASDU: 'Samples per packet',
43+
seqNum: 'Add Sequence Number',
44+
timeStamp: 'Add Timestamp',
45+
dataSet: 'Add DataSet Reference',
46+
reasonCode: 'Add Trigger Reason',
47+
dataRef: 'Add description of the payload',
48+
entryID: 'Add Entry ID',
49+
configRef: 'Add Configuration Revision',
50+
bufOvfl: 'Add Buffered Overflow information',
51+
indexed: 'Multiple instances possible',
52+
buffered: 'Buffered Report',
53+
maxReport: 'Number of Instances',
54+
bufTime: 'Min. time between two Reports',
55+
intgPd: 'Time between two periodic Reports',
4156
SmvOpts: 'Optional Information',
4257
refreshTime: 'Add timestamp to SMV packet',
4358
sampleRate: 'Add sample rate to SMV packet',
44-
dataSet: 'Add DataSet name to SMV packet',
4559
security: 'Potential future use. e.g. digital signature',
4660
synchSourceId: 'Add sync source id to SMV packet',
4761
},
@@ -54,8 +68,9 @@ export const en = {
5468
showieds: 'Show IEDs in substation editor',
5569
selectFileButton: 'Select file',
5670
loadNsdTranslations: 'Uploading NSDoc files',
57-
invalidFileNoIdFound: 'Invalid NSDoc; no \'id\' attribute found in file',
58-
invalidNsdocVersion: 'The version of {{ id }} NSD ({{ nsdVersion }}) does not correlate with the version of the corresponding NSDoc ({{ nsdocVersion }})'
71+
invalidFileNoIdFound: "Invalid NSDoc; no 'id' attribute found in file",
72+
invalidNsdocVersion:
73+
'The version of {{ id }} NSD ({{ nsdVersion }}) does not correlate with the version of the corresponding NSDoc ({{ nsdocVersion }})',
5974
},
6075
menu: {
6176
new: 'New project',
@@ -445,6 +460,13 @@ export const en = {
445460
yCoordinateHelper: 'Y-Coordinate for Single Line Diagram',
446461
},
447462
},
463+
dataset: {
464+
fcda: { add: 'Add Data Attributes' },
465+
fcd: { add: 'Add Data Objects' },
466+
},
467+
report: {
468+
wizard: { location: 'Select Report Control Location' },
469+
},
448470
add: 'Add',
449471
new: 'New',
450472
remove: 'Remove',

src/wizards/fcda.ts

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import { html } from 'lit-element';
2-
import { get, translate } from 'lit-translate';
2+
import { get } from 'lit-translate';
33

4-
import '../finder-list.js';
54
import {
65
createElement,
7-
identity,
86
selector,
97
Wizard,
108
WizardAction,
119
WizardActor,
1210
WizardInput,
1311
} from '../foundation.js';
14-
import { getChildren } from './foundation/functions.js';
15-
import { Directory, FinderList } from '../finder-list.js';
12+
import { FinderList } from '../finder-list.js';
13+
import {
14+
dataAttributePicker,
15+
getDataModelChildren,
16+
} from './foundation/finder.js';
1617

17-
function newFCDA(parent: Element, path: string[]): Element | undefined {
18+
export function newFCDA(parent: Element, path: string[]): Element | undefined {
1819
const [leafTag, leafId] = path[path.length - 1].split(': ');
1920
const leaf = parent.ownerDocument.querySelector(selector(leafTag, leafId));
20-
if (!leaf || getChildren(leaf).length > 0) return;
21+
if (!leaf || getDataModelChildren(leaf).length > 0) return;
2122

2223
const lnSegment = path.find(segment => segment.startsWith('LN'));
2324
if (!lnSegment) return;
@@ -31,7 +32,7 @@ function newFCDA(parent: Element, path: string[]): Element | undefined {
3132
const prefix = ln.getAttribute('prefix') ?? '';
3233
const lnClass = ln.getAttribute('lnClass');
3334
const lnInst =
34-
(ln.getAttribute('inst') && ln.getAttribute('inst') !== '')
35+
ln.getAttribute('inst') && ln.getAttribute('inst') !== ''
3536
? ln.getAttribute('inst')
3637
: null;
3738

@@ -95,28 +96,6 @@ function createFCDAsAction(parent: Element): WizardActor {
9596
};
9697
}
9798

98-
function getDisplayString(entry: string): string {
99-
return entry.replace(/^.*>/, '').trim();
100-
}
101-
102-
function getReader(server: Element): (path: string[]) => Promise<Directory> {
103-
return async (path: string[]) => {
104-
const [tagName, id] = path[path.length - 1]?.split(': ', 2);
105-
const element = server.ownerDocument.querySelector(selector(tagName, id));
106-
107-
if (!element)
108-
return { path, header: html`<p>${translate('error')}</p>`, entries: [] };
109-
110-
return {
111-
path,
112-
header: undefined,
113-
entries: getChildren(element).map(
114-
child => `${child.tagName}: ${identity(child)}`
115-
),
116-
};
117-
};
118-
}
119-
12099
export function createFCDAsWizard(parent: Element): Wizard {
121100
const server = parent.closest('Server');
122101

@@ -128,17 +107,7 @@ export function createFCDAsWizard(parent: Element): Wizard {
128107
icon: 'add',
129108
action: createFCDAsAction(parent),
130109
},
131-
content: [
132-
server
133-
? html`<finder-list
134-
multi
135-
.paths=${[['Server: ' + identity(server)]]}
136-
.read=${getReader(server)}
137-
.getDisplayString=${getDisplayString}
138-
.getTitle=${(path: string[]) => path[path.length - 1]}
139-
></finder-list>`
140-
: html``,
141-
],
110+
content: [server ? dataAttributePicker(server) : html``],
142111
},
143112
];
144113
}

src/wizards/foundation/finder.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { html, TemplateResult } from 'lit-element';
2+
import { translate } from 'lit-translate';
3+
4+
import '../../finder-list.js';
5+
import { Directory } from '../../finder-list.js';
6+
import { identity, isPublic, selector } from '../../foundation.js';
7+
8+
function getDisplayString(entry: string): string {
9+
if (entry.startsWith('IED:')) return entry.replace(/^.*:/, '').trim();
10+
if (entry.startsWith('LN0:')) return 'LLN0';
11+
return entry.replace(/^.*>/, '').trim();
12+
}
13+
14+
function getReader(
15+
server: Element,
16+
getChildren: (element: Element) => Element[]
17+
): (path: string[]) => Promise<Directory> {
18+
return async (path: string[]) => {
19+
const [tagName, id] = path[path.length - 1]?.split(': ', 2);
20+
const element = server.ownerDocument.querySelector(selector(tagName, id));
21+
22+
if (!element)
23+
return { path, header: html`<p>${translate('error')}</p>`, entries: [] };
24+
25+
return {
26+
path,
27+
header: undefined,
28+
entries: getChildren(element).map(
29+
child => `${child.tagName}: ${identity(child)}`
30+
),
31+
};
32+
};
33+
}
34+
35+
function getIED(parent: Element): Element[] {
36+
if (parent.tagName === 'SCL')
37+
return Array.from(parent.querySelectorAll('IED')).filter(isPublic);
38+
39+
return [];
40+
}
41+
42+
export function iEDPicker(doc: XMLDocument): TemplateResult {
43+
return html`<finder-list
44+
path="${JSON.stringify(['SCL: '])}"
45+
.read=${getReader(doc.querySelector('SCL')!, getIED)}
46+
.getDisplayString=${getDisplayString}
47+
.getTitle=${(path: string[]) => path[path.length - 1]}
48+
></finder-list>`;
49+
}
50+
51+
export function getDataModelChildren(parent: Element): Element[] {
52+
if (['LDevice', 'Server'].includes(parent.tagName))
53+
return Array.from(parent.children).filter(
54+
child =>
55+
child.tagName === 'LDevice' ||
56+
child.tagName === 'LN0' ||
57+
child.tagName === 'LN'
58+
);
59+
60+
const id =
61+
parent.tagName === 'LN' || parent.tagName === 'LN0'
62+
? parent.getAttribute('lnType')
63+
: parent.getAttribute('type');
64+
65+
return Array.from(
66+
parent.ownerDocument.querySelectorAll(
67+
`LNodeType[id="${id}"] > DO, DOType[id="${id}"] > SDO, DOType[id="${id}"] > DA, DAType[id="${id}"] > BDA`
68+
)
69+
);
70+
}
71+
72+
export function dataAttributePicker(server: Element): TemplateResult {
73+
return html`<finder-list
74+
multi
75+
.paths=${[['Server: ' + identity(server)]]}
76+
.read=${getReader(server, getDataModelChildren)}
77+
.getDisplayString=${getDisplayString}
78+
.getTitle=${(path: string[]) => path[path.length - 1]}
79+
></finder-list>`;
80+
}

src/wizards/foundation/functions.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)