Skip to content

Commit bfd43ba

Browse files
authored
fix(communication-plugin): Offer only valid connected aps as move targets (openscd#1685)
1 parent dd6e550 commit bfd43ba

File tree

7 files changed

+202
-13
lines changed

7 files changed

+202
-13
lines changed

.github/workflows/pr-preview.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ jobs:
2323
uses: actions/checkout@v3
2424
with:
2525
submodules: "true"
26+
27+
- name: Use Node.js 18.x
28+
uses: actions/setup-node@v1
29+
with:
30+
node-version: "18.x"
31+
2632
- name: Install and Build OpenSCD
2733
if: github.event.action != 'closed' # You might want to skip the build if the PR has been closed
2834
run: |

packages/plugins/src/editors/communication/foundation.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,78 @@ export function getAllConnectedAPsOfSameIED(
3333
doc.querySelectorAll(`SubNetwork > ConnectedAP[iedName=${iedName}`)
3434
);
3535
}
36+
37+
type communicationElementTag = 'GSE' | 'SMV';
38+
39+
const controlTagDictionary: { [key in communicationElementTag]: string } = {
40+
GSE: 'GSEControl',
41+
SMV: 'SampledValueControl'
42+
};
43+
44+
export function canMoveCommunicationElementToConnectedAP(
45+
communicationElement: Element,
46+
connectedAP: Element,
47+
doc: XMLDocument
48+
): boolean {
49+
const currentConnectedAP = getCurrentConnectedAP(communicationElement);
50+
51+
if (!currentConnectedAP || currentConnectedAP === connectedAP) {
52+
return false;
53+
}
54+
55+
const apName = currentConnectedAP.getAttribute('apName');
56+
const iedName = currentConnectedAP.getAttribute('iedName');
57+
58+
if (!apName || !iedName) {
59+
return false;
60+
}
61+
62+
const ied = doc.querySelector(`IED[name=${iedName}]`);
63+
64+
if (!ied) {
65+
return false;
66+
}
67+
68+
const targetApName = connectedAP.getAttribute('apName');
69+
const targetAp = ied.querySelector(`:scope > AccessPoint[name=${targetApName}]`);
70+
71+
if (!targetAp || !targetApName) {
72+
return false;
73+
}
74+
75+
const server = queryServer(ied, targetApName);
76+
if (!server) {
77+
return false;
78+
}
79+
80+
const ldInst = communicationElement.getAttribute('ldInst');
81+
const cbName = communicationElement.getAttribute('cbName');
82+
const controlTag = controlTagDictionary[communicationElement.tagName as communicationElementTag];
83+
84+
const controlElement = server.querySelector(`:scope > LDevice[inst=${ldInst}] ${controlTag}[name=${cbName}]`);
85+
const serverHasControl = controlElement !== null;
86+
87+
return serverHasControl;
88+
}
89+
90+
function queryServer(ied: Element, apName: string): Element | null {
91+
const accessPoint = ied.querySelector(`AccessPoint[name=${apName}]`);
92+
if (!accessPoint) {
93+
return null;
94+
}
95+
96+
const server = accessPoint.querySelector('Server');
97+
if (server) {
98+
return server;
99+
}
100+
101+
const serverAt = accessPoint.querySelector('ServerAt');
102+
const serverApName = serverAt?.getAttribute('apName');
103+
if (!serverApName) {
104+
return null;
105+
}
106+
107+
const accessPointWithServer = ied.querySelector(`AccessPoint[name=${serverApName}]`);
108+
109+
return accessPointWithServer?.querySelector('Server') ?? null;
110+
}

packages/plugins/src/editors/communication/gse-editor.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { newWizardEvent } from '@openscd/open-scd/src/foundation.js';
1515
import { newActionEvent } from '@openscd/core/foundation/deprecated/editor.js';
1616
import { sizableGooseIcon } from '@openscd/open-scd/src/icons/icons.js';
1717
import { editGseWizard } from '../../wizards/gse.js';
18-
import { getAllConnectedAPsOfSameIED } from './foundation.js';
18+
import { canMoveCommunicationElementToConnectedAP, getAllConnectedAPsOfSameIED } from './foundation.js';
1919

2020
@customElement('gse-editor')
2121
export class GseEditor extends LitElement {
@@ -62,11 +62,15 @@ export class GseEditor extends LitElement {
6262
}
6363

6464
render(): TemplateResult {
65-
const allConnectedAPsOfSameIED = getAllConnectedAPsOfSameIED(
65+
const validTargetConnectedAPs = getAllConnectedAPsOfSameIED(
6666
this.element,
6767
this.doc
68-
);
69-
const hasMoreThanOneConnectedAP = allConnectedAPsOfSameIED.length > 1;
68+
).filter(cap => canMoveCommunicationElementToConnectedAP(
69+
this.element!,
70+
cap,
71+
this.doc
72+
));
73+
const hasValidConnectedAPMoveTarget = validTargetConnectedAPs.length > 0;
7074

7175
return html`<action-icon label="${this.label}" .icon="${sizableGooseIcon}"
7276
><mwc-fab
@@ -86,7 +90,7 @@ export class GseEditor extends LitElement {
8690
mini
8791
icon="forward"
8892
class="gse-move-button"
89-
?disabled="${!hasMoreThanOneConnectedAP}"
93+
?disabled="${!hasValidConnectedAPMoveTarget}"
9094
@click="${() => this.openGseMoveDialog()}"
9195
>
9296
</mwc-fab>

packages/plugins/src/editors/communication/smv-editor.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { sizableSmvIcon } from '@openscd/open-scd/src/icons/icons.js';
1515
import { newWizardEvent } from '@openscd/open-scd/src/foundation.js';
1616
import { newActionEvent } from '@openscd/core/foundation/deprecated/editor.js';
1717
import { editSMvWizard } from '../../wizards/smv.js';
18-
import { getAllConnectedAPsOfSameIED } from './foundation.js';
18+
import { canMoveCommunicationElementToConnectedAP, getAllConnectedAPsOfSameIED } from './foundation.js';
1919

2020
@customElement('smv-editor')
2121
export class SmvEditor extends LitElement {
@@ -62,11 +62,15 @@ export class SmvEditor extends LitElement {
6262
}
6363

6464
render(): TemplateResult {
65-
const allConnectedAPsOfSameIED = getAllConnectedAPsOfSameIED(
65+
const validTargetConnectedAPs = getAllConnectedAPsOfSameIED(
6666
this.element,
6767
this.doc
68-
);
69-
const hasMoreThanOneConnectedAP = allConnectedAPsOfSameIED.length > 1;
68+
).filter(cap => canMoveCommunicationElementToConnectedAP(
69+
this.element!,
70+
cap,
71+
this.doc
72+
));
73+
const hasValidConnectedAPMoveTarget = validTargetConnectedAPs.length > 0;
7074

7175
return html`<action-icon label="${this.label}" .icon="${sizableSmvIcon}"
7276
><mwc-fab
@@ -86,7 +90,7 @@ export class SmvEditor extends LitElement {
8690
mini
8791
icon="forward"
8892
class="smv-move-button"
89-
?disabled=${!hasMoreThanOneConnectedAP}
93+
?disabled=${!hasValidConnectedAPMoveTarget}
9094
@click="${() => this.openSmvMoveDialog()}"
9195
>
9296
</mwc-fab>

packages/plugins/src/editors/communication/subnetwork-editor.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import { newActionEvent } from '@openscd/core/foundation/deprecated/editor.js';
2525
import { createConnectedApWizard } from '../../wizards/connectedap.js';
2626
import { wizards } from '../../wizards/wizard-library.js';
27-
import { getAllConnectedAPsOfSameIED } from './foundation.js';
27+
import { canMoveCommunicationElementToConnectedAP, getAllConnectedAPsOfSameIED } from './foundation.js';
2828

2929
/** [[`Communication`]] subeditor for a `SubNetwork` element. */
3030
@customElement('subnetwork-editor')
@@ -200,7 +200,12 @@ export class SubNetworkEditor extends LitElement {
200200
this.moveTargetElement,
201201
this.doc
202202
);
203-
const currentConnectedAP = this.moveTargetElement.closest('ConnectedAP');
203+
204+
const validTargetConnectedAPs = allConnectedAPs.filter(cap => canMoveCommunicationElementToConnectedAP(
205+
this.moveTargetElement!,
206+
cap,
207+
this.doc
208+
));
204209

205210
return html`
206211
<mwc-dialog
@@ -218,7 +223,7 @@ export class SubNetworkEditor extends LitElement {
218223
class=${connectedAP === this.newlySelectedAP ? 'selected' : ''}
219224
@click=${() => (this.newlySelectedAP = connectedAP)}
220225
?selected=${connectedAP === this.newlySelectedAP}
221-
?disabled=${connectedAP === currentConnectedAP}
226+
?disabled=${!validTargetConnectedAPs.includes(connectedAP)}
222227
>
223228
${connectedAP.getAttribute('iedName')} >
224229
${connectedAP.getAttribute('apName')}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007" revision="B" release="4">
3+
<Communication>
4+
<SubNetwork name="SN1" type="Ethernet">
5+
<ConnectedAP iedName="IED1" apName="AP1">
6+
</ConnectedAP>
7+
<ConnectedAP iedName="IED1" apName="AP3">
8+
</ConnectedAP>
9+
</SubNetwork>
10+
<SubNetwork name="SN2" type="Ethernet">
11+
<ConnectedAP iedName="IED1" apName="AP2">
12+
<GSE ldInst="LD1" cbName="GSE1">
13+
</GSE>
14+
<SMV ldInst="LD1" cbName="SM1">
15+
</SMV>
16+
</ConnectedAP>
17+
<ConnectedAP iedName="IED1" apName="AP4">
18+
</ConnectedAP>
19+
</SubNetwork>
20+
</Communication>
21+
<IED name="IED1">
22+
<AccessPoint name="AP1" type="IED">
23+
<Server>
24+
<LDevice inst="LD1">
25+
<LN0 lnClass="LLN0" lnInst="INST1">
26+
<GSEControl name="GSE1" desc="GSE Control Example" appID="GSEApp1" confRev="1" />
27+
<SampledValueControl name="SM1" />
28+
</LN0>
29+
</LDevice>
30+
</Server>
31+
</AccessPoint>
32+
<AccessPoint name="AP2">
33+
<ServerAt apName="AP1" />
34+
</AccessPoint>
35+
<AccessPoint name="AP3">
36+
</AccessPoint>
37+
<AccessPoint name="AP4">
38+
<ServerAt apName="AP1" />
39+
</AccessPoint>
40+
</IED>
41+
</SCL>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { expect } from '@open-wc/testing';
2+
import { getAllConnectedAPsOfSameIED, canMoveCommunicationElementToConnectedAP } from '../../../../src/editors/communication/foundation.js';
3+
4+
describe('Communication plugin foundation', () => {
5+
let doc: XMLDocument;
6+
7+
beforeEach(async () => {
8+
doc = await fetch('/test/testfiles/communication/move-gse-testfile.scd')
9+
.then(response => response.text())
10+
.then(str => new DOMParser().parseFromString(str, 'application/xml'));
11+
});
12+
13+
it('should determine if gse can be moved to connected AP', () => {
14+
const gseElement = doc.querySelector('GSE')!;
15+
16+
const aps = getAllConnectedAPsOfSameIED(gseElement, doc);
17+
18+
const connectedAP1 = aps.find(ap => ap.getAttribute('apName') === 'AP1')!;
19+
const connectedAP2 = aps.find(ap => ap.getAttribute('apName') === 'AP2')!;
20+
const connectedAP3 = aps.find(ap => ap.getAttribute('apName') === 'AP3')!;
21+
const connectedAP4 = aps.find(ap => ap.getAttribute('apName') === 'AP4')!;
22+
23+
const canMoveToAP1 = canMoveCommunicationElementToConnectedAP(gseElement, connectedAP1, doc);
24+
const canMoveToAP2 = canMoveCommunicationElementToConnectedAP(gseElement, connectedAP2, doc);
25+
const canMoveToAP3 = canMoveCommunicationElementToConnectedAP(gseElement, connectedAP3, doc);
26+
const canMoveToAP4 = canMoveCommunicationElementToConnectedAP(gseElement, connectedAP4, doc);
27+
28+
expect(canMoveToAP1).to.be.true;
29+
expect(canMoveToAP2).to.be.false;
30+
expect(canMoveToAP3).to.be.false;
31+
expect(canMoveToAP4).to.be.true;
32+
});
33+
34+
it('should determine if smv can be moved to connected AP', () => {
35+
const smvElement = doc.querySelector('SMV')!;
36+
37+
const aps = getAllConnectedAPsOfSameIED(smvElement, doc);
38+
39+
const connectedAP1 = aps.find(ap => ap.getAttribute('apName') === 'AP1')!;
40+
const connectedAP2 = aps.find(ap => ap.getAttribute('apName') === 'AP2')!;
41+
const connectedAP3 = aps.find(ap => ap.getAttribute('apName') === 'AP3')!;
42+
const connectedAP4 = aps.find(ap => ap.getAttribute('apName') === 'AP4')!;
43+
44+
const canMoveToAP1 = canMoveCommunicationElementToConnectedAP(smvElement, connectedAP1, doc);
45+
const canMoveToAP2 = canMoveCommunicationElementToConnectedAP(smvElement, connectedAP2, doc);
46+
const canMoveToAP3 = canMoveCommunicationElementToConnectedAP(smvElement, connectedAP3, doc);
47+
const canMoveToAP4 = canMoveCommunicationElementToConnectedAP(smvElement, connectedAP4, doc);
48+
49+
expect(canMoveToAP1).to.be.true;
50+
expect(canMoveToAP2).to.be.false;
51+
expect(canMoveToAP3).to.be.false;
52+
expect(canMoveToAP4).to.be.true;
53+
});
54+
});

0 commit comments

Comments
 (0)