Skip to content

Commit 4b48f6e

Browse files
author
Dennis Labordus
authored
bug(editor/subscriber): When selecting ExtRef also include Control Reference Attributes (GOOSE/SMV) (openscd#1041)
* Added plugin and show FCDA Elements including edit button * Added plugin and show FCDA Elements including edit button * Added plugin and show FCDA Elements including edit button + renaming other subscriber plugins * Fixed plugin test * Show connected and available LN + small fixes * First version of subscribe and unsubscribe * Added test for subscribing and unsubscribing logica node binding * First step for the counter, refactoring list methods to re-use logic. Also removed sorting. * Added counter logic to both subscriber plugins * Processed review comments. * Changed query to retrieve available LN Classes to match only data binding ExtRefs * Fix performance issue. * Fixed snapshot. * Fix bug when selecting ExtRef also include Control References. * Fixed tests. * Review comments processed * Fixed tests. * Fixed test.
1 parent 81487c1 commit 4b48f6e

15 files changed

+1777
-2022
lines changed

src/editors/subscription/foundation.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,57 @@ export function getFcdaTitleValue(fcdaElement: Element): string {
9797
}${fcdaElement.getAttribute('daName')}`;
9898
}
9999

100-
export function existExtRef(parentInputs: Element, fcda: Element): boolean {
100+
export function existExtRef(
101+
parentInputs: Element,
102+
fcda: Element,
103+
control: Element | undefined
104+
): boolean {
105+
return !!getExtRef(parentInputs, fcda, control);
106+
}
107+
108+
export function getExtRef(
109+
parentInputs: Element,
110+
fcda: Element,
111+
control: Element | undefined
112+
): Element | undefined {
113+
function createCriteria(attributeName: string, value: string | null): string {
114+
if (value) {
115+
return `[${attributeName}="${value}"]`;
116+
}
117+
return '';
118+
}
119+
101120
const iedName = fcda.closest('IED')?.getAttribute('name');
102-
if (!iedName) return false;
121+
if (!iedName) {
122+
return undefined;
123+
}
103124

104-
return !!parentInputs.querySelector(
105-
`ExtRef[iedName=${iedName}]` + `${getFcdaReferences(fcda)}`
106-
);
125+
let controlCriteria = '';
126+
if (control && getSclSchemaVersion(fcda.ownerDocument) !== '2003') {
127+
controlCriteria = `[serviceType="${serviceTypes[control.tagName]!}"]`;
128+
controlCriteria += createCriteria(
129+
'srcLDInst',
130+
control.closest('LDevice')?.getAttribute('inst') ?? null
131+
);
132+
controlCriteria += createCriteria(
133+
'srcLNClass',
134+
control.closest('LN0,LN')?.getAttribute('lnClass') ?? null
135+
);
136+
controlCriteria += createCriteria(
137+
'srcLNInst',
138+
control.closest('LN0,LN')?.getAttribute('inst') ?? null
139+
);
140+
controlCriteria += createCriteria(
141+
'srcCBName',
142+
control.getAttribute('name') ?? null
143+
);
144+
}
145+
146+
return Array.from(
147+
parentInputs.querySelectorAll(
148+
`ExtRef[iedName="${iedName}"]${getFcdaReferences(fcda)}${controlCriteria}`
149+
)
150+
).find(extRefElement => !extRefElement.hasAttribute('intAddr'));
107151
}
108152

109153
export function canCreateValidExtRef(
@@ -117,25 +161,30 @@ export function canCreateValidExtRef(
117161
'lnInst',
118162
'doName',
119163
].map(attr => fcda.getAttribute(attr));
164+
if (!iedName || !ldInst || !lnClass || !lnInst || !doName) {
165+
return false;
166+
}
120167

121-
if (!iedName || !ldInst || !lnClass || !lnInst || !doName) return false;
122-
123-
if (controlBlock === undefined) return true; //for serviceType `Poll`
168+
// For 2003 schema or serviceType `Poll`, the extra fields aren't needed.
169+
if (
170+
getSclSchemaVersion(fcda.ownerDocument) === '2003' ||
171+
controlBlock === undefined
172+
) {
173+
return true;
174+
}
124175

125176
const srcLDInst = controlBlock.closest('LDevice')?.getAttribute('inst');
126177
const srcLNClass = controlBlock.closest('LN0,LN')?.getAttribute('lnClass');
127178
const srcLNInst = controlBlock.closest('LN0,LN')?.getAttribute('inst');
128179
const srcCBName = controlBlock.getAttribute('name');
129180

130-
if (
181+
// For srcLNInst an empty string is allowed in `LN0`
182+
return !(
131183
!srcLDInst ||
132184
!srcLNClass ||
133185
!srcCBName ||
134-
typeof srcLNInst !== 'string' //empty string is allowed in `LN0`
135-
)
136-
return false;
137-
138-
return true;
186+
typeof srcLNInst !== 'string'
187+
);
139188
}
140189

141190
export const serviceTypes: Partial<Record<string, string>> = {

src/editors/subscription/goose/subscriber-list.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ import {
2424
GooseSubscriptionEvent,
2525
newGooseSubscriptionEvent,
2626
} from './foundation.js';
27-
import {
28-
emptyInputsDeleteActions,
29-
getFcdaReferences,
30-
} from '../../../foundation/ied.js';
27+
import { emptyInputsDeleteActions } from '../../../foundation/ied.js';
3128
import {
3229
canCreateValidExtRef,
3330
createExtRefElement,
3431
existExtRef,
32+
getExtRef,
3533
IEDSelectEvent,
3634
ListElement,
3735
styles,
@@ -109,12 +107,7 @@ export class SubscriberList extends SubscriberListContainer {
109107

110108
dataSet!.querySelectorAll('FCDA').forEach(fcda => {
111109
subscribedInputs.forEach(inputs => {
112-
if (
113-
inputs.querySelector(
114-
`ExtRef[iedName=${ied.getAttribute('name')}]` +
115-
`${getFcdaReferences(fcda)}`
116-
)
117-
) {
110+
if (getExtRef(inputs, fcda, this.currentSelectedGseControl)) {
118111
numberOfLinkedExtRefs++;
119112
}
120113
});
@@ -166,12 +159,7 @@ export class SubscriberList extends SubscriberListContainer {
166159
*/
167160
this.currentUsedDataset!.querySelectorAll('FCDA').forEach(fcda => {
168161
inputElements.forEach(inputs => {
169-
if (
170-
inputs.querySelector(
171-
`ExtRef[iedName=${this.currentGooseIedName}]` +
172-
`${getFcdaReferences(fcda)}`
173-
)
174-
) {
162+
if (getExtRef(inputs, fcda, this.currentSelectedGseControl)) {
175163
numberOfLinkedExtRefs++;
176164
}
177165
});
@@ -251,7 +239,7 @@ export class SubscriberList extends SubscriberListContainer {
251239
const actions: Create[] = [];
252240
this.currentUsedDataset!.querySelectorAll('FCDA').forEach(fcda => {
253241
if (
254-
!existExtRef(inputsElement!, fcda) &&
242+
!existExtRef(inputsElement!, fcda, this.currentSelectedGseControl) &&
255243
canCreateValidExtRef(fcda, this.currentSelectedGseControl)
256244
) {
257245
const extRef = createExtRefElement(
@@ -281,11 +269,7 @@ export class SubscriberList extends SubscriberListContainer {
281269
const actions: Delete[] = [];
282270
ied.querySelectorAll('LN0 > Inputs, LN > Inputs').forEach(inputs => {
283271
this.currentUsedDataset!.querySelectorAll('FCDA').forEach(fcda => {
284-
const extRef = inputs.querySelector(
285-
`ExtRef[iedName=${this.currentGooseIedName}]` +
286-
`${getFcdaReferences(fcda)}`
287-
);
288-
272+
const extRef = getExtRef(inputs, fcda, this.currentSelectedGseControl);
289273
if (extRef) actions.push({ old: { parent: inputs, element: extRef } });
290274
});
291275
});

src/editors/subscription/later-binding/ext-ref-ln-binding-list.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ import {
2424
createExtRefElement,
2525
existExtRef,
2626
FcdaSelectEvent,
27+
getExtRef,
2728
newSubscriptionChangedEvent,
2829
styles,
2930
} from '../foundation.js';
3031
import { getSubscribedExtRefElements } from './foundation.js';
31-
import {
32-
emptyInputsDeleteActions,
33-
getFcdaReferences,
34-
} from '../../../foundation/ied.js';
32+
import { emptyInputsDeleteActions } from '../../../foundation/ied.js';
3533

3634
/**
3735
* A sub element for showing all Ext Refs from a FCDA Element.
@@ -126,7 +124,11 @@ export class ExtRefLnBindingList extends LitElement {
126124
}
127125

128126
if (
129-
!existExtRef(inputsElement!, this.currentSelectedFcdaElement) &&
127+
!existExtRef(
128+
inputsElement!,
129+
this.currentSelectedFcdaElement,
130+
this.currentSelectedControlElement
131+
) &&
130132
canCreateValidExtRef(
131133
this.currentSelectedFcdaElement,
132134
this.currentSelectedControlElement
@@ -154,9 +156,10 @@ export class ExtRefLnBindingList extends LitElement {
154156

155157
const actions: Delete[] = [];
156158
const inputElement = lnElement.querySelector(':scope > Inputs')!;
157-
const extRefElement = inputElement.querySelector(
158-
`ExtRef[iedName=${this.currentIedElement.getAttribute('name')}]` +
159-
`${getFcdaReferences(this.currentSelectedFcdaElement)}`
159+
const extRefElement = getExtRef(
160+
inputElement,
161+
this.currentSelectedFcdaElement,
162+
this.currentSelectedControlElement
160163
);
161164
if (extRefElement) {
162165
actions.push({ old: { parent: inputElement, element: extRefElement } });

src/editors/subscription/sampledvalues/subscriber-list.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ import {
2424
SmvSelectEvent,
2525
SmvSubscriptionEvent,
2626
} from './foundation.js';
27-
import {
28-
emptyInputsDeleteActions,
29-
getFcdaReferences,
30-
} from '../../../foundation/ied.js';
27+
import { emptyInputsDeleteActions } from '../../../foundation/ied.js';
3128
import {
3229
canCreateValidExtRef,
3330
createExtRefElement,
3431
existExtRef,
32+
getExtRef,
3533
IEDSelectEvent,
3634
ListElement,
3735
styles,
@@ -109,12 +107,7 @@ export class SubscriberList extends SubscriberListContainer {
109107

110108
dataSet!.querySelectorAll('FCDA').forEach(fcda => {
111109
subscribedInputs.forEach(inputs => {
112-
if (
113-
inputs.querySelector(
114-
`ExtRef[iedName=${ied.getAttribute('name')}]` +
115-
`${getFcdaReferences(fcda)}`
116-
)
117-
) {
110+
if (getExtRef(inputs, fcda, this.currentSelectedSmvControl)) {
118111
numberOfLinkedExtRefs++;
119112
}
120113
});
@@ -168,12 +161,7 @@ export class SubscriberList extends SubscriberListContainer {
168161
*/
169162
this.currentUsedDataset!.querySelectorAll('FCDA').forEach(fcda => {
170163
inputElements.forEach(inputs => {
171-
if (
172-
inputs.querySelector(
173-
`ExtRef[iedName=${this.currentSmvIedName}]` +
174-
`${getFcdaReferences(fcda)}`
175-
)
176-
) {
164+
if (getExtRef(inputs, fcda, this.currentSelectedSmvControl)) {
177165
numberOfLinkedExtRefs++;
178166
}
179167
});
@@ -253,7 +241,7 @@ export class SubscriberList extends SubscriberListContainer {
253241
const actions: Create[] = [];
254242
this.currentUsedDataset!.querySelectorAll('FCDA').forEach(fcda => {
255243
if (
256-
!existExtRef(inputsElement!, fcda) &&
244+
!existExtRef(inputsElement!, fcda, this.currentSelectedSmvControl) &&
257245
canCreateValidExtRef(fcda, this.currentSelectedSmvControl)
258246
) {
259247
const extRef = createExtRefElement(
@@ -283,11 +271,7 @@ export class SubscriberList extends SubscriberListContainer {
283271
const actions: Delete[] = [];
284272
ied.querySelectorAll('LN0 > Inputs, LN > Inputs').forEach(inputs => {
285273
this.currentUsedDataset!.querySelectorAll('FCDA').forEach(fcda => {
286-
const extRef = inputs.querySelector(
287-
`ExtRef[iedName=${this.currentSmvIedName}]` +
288-
`${getFcdaReferences(fcda)}`
289-
);
290-
274+
const extRef = getExtRef(inputs, fcda, this.currentSelectedSmvControl);
291275
if (extRef) actions.push({ old: { parent: inputs, element: extRef } });
292276
});
293277
});

src/foundation/ied.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
Delete,
3-
identity,
4-
selector
5-
} from "../foundation.js";
1+
import { Delete, identity, selector } from '../foundation.js';
62

73
/**
84
* All available FCDA references that are used to link ExtRefs.
@@ -22,13 +18,36 @@ const fcdaReferences = [
2218
* @param elementContainingFcdaReferences - The element to use
2319
* @returns FCDA references
2420
*/
25-
export function getFcdaReferences(elementContainingFcdaReferences: Element): string {
21+
export function getFcdaReferences(
22+
elementContainingFcdaReferences: Element
23+
): string {
2624
return fcdaReferences
2725
.map(fcdaRef =>
2826
elementContainingFcdaReferences.getAttribute(fcdaRef)
2927
? `[${fcdaRef}="${elementContainingFcdaReferences.getAttribute(
30-
fcdaRef
31-
)}"]`
28+
fcdaRef
29+
)}"]`
30+
: ''
31+
)
32+
.join('');
33+
}
34+
35+
/**
36+
* All available Control references that are used to link ExtRefs.
37+
*/
38+
const controlReferences = ['srcLDInst', 'srcLNClass', 'srcLNInst', 'srcCBName'];
39+
40+
/**
41+
* Get all the Control attributes containing values from a specific element.
42+
*
43+
* @param extRef - The element to use
44+
* @returns Control references
45+
*/
46+
export function getControlReferences(extRef: Element): string {
47+
return controlReferences
48+
.map(controlRef =>
49+
extRef.getAttribute(controlRef)
50+
? `[${controlRef}="${extRef.getAttribute(controlRef)}"]`
3251
: ''
3352
)
3453
.join('');
@@ -40,7 +59,9 @@ export function getFcdaReferences(elementContainingFcdaReferences: Element): str
4059
* @param extRefDeleteActions - All Delete actions for ExtRefs.
4160
* @returns Possible delete actions for empty Inputs elements.
4261
*/
43-
export function emptyInputsDeleteActions(extRefDeleteActions: Delete[]): Delete[] {
62+
export function emptyInputsDeleteActions(
63+
extRefDeleteActions: Delete[]
64+
): Delete[] {
4465
if (!extRefDeleteActions.length) return [];
4566

4667
const inputDeleteActions: Delete[] = [];
@@ -51,18 +72,27 @@ export function emptyInputsDeleteActions(extRefDeleteActions: Delete[]): Delete[
5172
const inputsElement = <Element>extRefDeleteAction.old.parent;
5273

5374
const id = identity(inputsElement);
54-
if (!inputsMap[id]) inputsMap[id] = <Element>(inputsElement.cloneNode(true));
75+
if (!inputsMap[id]) inputsMap[id] = <Element>inputsElement.cloneNode(true);
5576

5677
// Search the ExtRef in the Cloned Inputs Element
57-
const linkedExtRef = inputsMap[id].querySelector(`ExtRef[iedName=${extRef.getAttribute('iedName')}]` +
58-
`${getFcdaReferences(extRef)}`);
78+
const linkedExtRef = inputsMap[id].querySelector(
79+
`ExtRef${
80+
extRef.getAttribute('iedName')
81+
? `[iedName="${extRef.getAttribute('iedName')}"]`
82+
: ''
83+
}${getFcdaReferences(extRef)}${
84+
extRef.getAttribute('serviceType')
85+
? `[serviceType="${extRef.getAttribute('serviceType')}"]`
86+
: ''
87+
}${getControlReferences(extRef)}`
88+
);
5989
// And if found remove it as child from the Cloned Inputs Element
6090
if (linkedExtRef) inputsMap[id].removeChild(linkedExtRef);
6191
}
6292

6393
// Create delete action for each empty inputs
6494
Object.entries(inputsMap).forEach(([key, value]) => {
65-
if (value.children.length ! == 0) {
95+
if (value.children.length! == 0) {
6696
const doc = extRefDeleteActions[0].old.parent.ownerDocument!;
6797
const inputs = doc.querySelector(selector('Inputs', key));
6898

test/integration/editors/GooseSubscriberMessageBinding.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('GOOSE subscriber plugin', () => {
1717
let doc: XMLDocument;
1818

1919
beforeEach(async () => {
20-
doc = await fetch('/test/testfiles/valid2007B4ForSubscription.scd')
20+
doc = await fetch('/test/testfiles/editors/MessageBindinGOOSE2007B4.scd')
2121
.then(response => response.text())
2222
.then(str => new DOMParser().parseFromString(str, 'application/xml'));
2323

0 commit comments

Comments
 (0)