Skip to content

Commit

Permalink
feat(wizards/gsecontrol): add create wizards (openscd#654)
Browse files Browse the repository at this point in the history
* refactor(wizards/address): change content input definition

* refactor(wizards/gsecontrol): change content input definition

* refactor(wizard/gsecontrol): better naming

* refactor(wizards/address): change content function api

* feat(wizards/foundation/scl): add function to get unique MAC and APPID

* feat(wizards/foundation/scl): communication connection checks

* feat(wizards/gsecontrol): add create wizards

* test(wizards/gsecontrol): add unit tests

* test(wizards/gsecontrol): update snapshot

* test(wizards/gsecontrol): add integration tests

* test(wiazrds/foundation/scl): increate timeout in critical tests

* test(wiazrds/foundation/scl): remove time extensive tests

* fix review commit

* refactor(editors/gsecontrol): add warning message with missing communication connection

* fix(src/translations/en): spelling error

Co-authored-by: danyill <danyill@users.noreply.github.com>

* test(test/unit/wizards/foundation/scl): spelling

Co-authored-by: danyill <danyill@users.noreply.github.com>

* fix(test/unit/wizards/foundation/scl): incorrect element tag

Co-authored-by: danyill <danyill@users.noreply.github.com>

* fix(test/unit/wizards/foundation/scl): incorrect element tag

Co-authored-by: danyill <danyill@users.noreply.github.com>

* test(test/unit/wizards/foundation/scl): fix spelling

Co-authored-by: danyill <danyill@users.noreply.github.com>

* test(test/unit/wizards/foundation/scl): fix spelling

Co-authored-by: danyill <danyill@users.noreply.github.com>

* test(test/unit/wizards/foundation/scl): fix spelling

Co-authored-by: danyill <danyill@users.noreply.github.com>

* refactor: triggered by review

Co-authored-by: danyill <danyill@users.noreply.github.com>
  • Loading branch information
JakobVogelsang and danyill authored Apr 24, 2022
1 parent 3689ef2 commit 887f46f
Show file tree
Hide file tree
Showing 13 changed files with 1,693 additions and 132 deletions.
14 changes: 11 additions & 3 deletions src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export const de: Translations = {
showieds: 'Zeige IEDs im Substation-Editor',
selectFileButton: 'Datei auswählen',
loadNsdTranslations: 'NSDoc-Dateien hochladen',
invalidFileNoIdFound: "Ungültiges NSDoc ({{ filename }}); kein 'id'-Attribut in der Datei gefunden",
invalidFileNoIdFound:
"Ungültiges NSDoc ({{ filename }}); kein 'id'-Attribut in der Datei gefunden",
invalidNsdocVersion:
'Die Version {{ id }} NSD ({{ nsdVersion }}) passt nicht zu der geladenen NSDoc ({{ filename }}, {{ nsdocVersion }})',
},
Expand Down Expand Up @@ -499,6 +500,8 @@ export const de: Translations = {
action: {
addaddress: 'GSE bearbeitet ({{identity}})',
},
missingaccp:
'AccessPoint is nicht verbunden. GSE kann nicht hinzugefügt werden.',
},
smv: {
action: {
Expand Down Expand Up @@ -552,8 +555,10 @@ export const de: Translations = {
unreferencedControls: {
title: 'Steuerblöcke mit einem fehlenden oder ungültigen Kontrollblock',
deleteButton: 'Ausgewählte Kontrollblöcke entfernen',
tooltip: 'Steuerblöcke ohne Verweis auf ein vorhandenes Datensatz. Das ist kein Fehler und eher üblich for allem für Reports',
addressDefinitionTooltip: 'Für diesen Kontrollblock existiert eine Adressdefinition im Abschnitt Kommunikation',
tooltip:
'Steuerblöcke ohne Verweis auf ein vorhandenes Datensatz. Das ist kein Fehler und eher üblich for allem für Reports',
addressDefinitionTooltip:
'Für diesen Kontrollblock existiert eine Adressdefinition im Abschnitt Kommunikation',
alsoRemoveFromCommunication: 'Kommunikation SMV/GSE mit entfernen',
},
},
Expand All @@ -575,6 +580,9 @@ export const de: Translations = {
copy: 'Kopie in anderen IEDs ertellen',
},
},
gsecontrol: {
wizard: { location: 'Ablageort der GOOSE wählen' },
},
add: 'Hinzufügen',
new: 'Neu',
remove: 'Entfernen',
Expand Down
13 changes: 10 additions & 3 deletions src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export const en = {
showieds: 'Show IEDs in substation editor',
selectFileButton: 'Select file',
loadNsdTranslations: 'Uploaded NSDoc files',
invalidFileNoIdFound: "Invalid NSDoc ({{ filename }}); no 'id' attribute found in file",
invalidFileNoIdFound:
"Invalid NSDoc ({{ filename }}); no 'id' attribute found in file",
invalidNsdocVersion:
'The version of {{ id }} NSD ({{ nsdVersion }}) does not correlate with the version of the corresponding NSDoc ({{ filename }}, {{ nsdocVersion }})',
},
Expand Down Expand Up @@ -496,6 +497,7 @@ export const en = {
action: {
addaddress: 'Edit GSE ({{identity}})',
},
missingaccp: 'AccessPoint is not connected. GSE cannot be created.',
},
smv: {
action: {
Expand Down Expand Up @@ -549,8 +551,10 @@ export const en = {
unreferencedControls: {
title: 'Control Blocks with a Missing or Invalid Dataset',
deleteButton: 'Remove Selected Control Blocks',
tooltip: 'Control Blocks without a reference to an existing DataSet. Note that this is normal in an ICD file or for an MMS ReportControl with a dynamically allocated DataSet',
addressDefinitionTooltip: 'An address definition exists for this control block in the Communication section',
tooltip:
'Control Blocks without a reference to an existing DataSet. Note that this is normal in an ICD file or for an MMS ReportControl with a dynamically allocated DataSet',
addressDefinitionTooltip:
'An address definition exists for this control block in the Communication section',
alsoRemoveFromCommunication: 'Also remove SMV/GSE Address',
},
},
Expand All @@ -570,6 +574,9 @@ export const en = {
},
label: { copy: 'Copy to other IEDs' },
},
gsecontrol: {
wizard: { location: 'Select GOOSE Control Block Location' },
},
add: 'Add',
new: 'New',
remove: 'Remove',
Expand Down
60 changes: 26 additions & 34 deletions src/wizards/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,31 @@ import '@material/mwc-checkbox';
import '@material/mwc-formfield';

import '../wizard-textfield.js';
import {
Create,
createElement,
Delete,
getValue,
WizardInputElement,
} from '../foundation.js';
import {
pTypesGSESMV,
typeNullable,
typePattern,
} from './foundation/p-types.js';
import { Create, createElement, Delete } from '../foundation.js';
import { typeNullable, typePattern } from './foundation/p-types.js';

export function renderGseSmvAddress(parent: Element): TemplateResult[] {
const hasInstType = Array.from(parent.querySelectorAll('Address > P')).some(
pType => pType.getAttribute('xsi:type')
);
interface ContentOptions {
hasInstType: boolean;
attributes: Record<string, string | null>;
}

export function contentGseWizard(content: ContentOptions): TemplateResult[] {
return [
html`<mwc-formfield
label="${translate('connectedap.wizard.addschemainsttype')}"
>
<mwc-checkbox id="instType" ?checked="${hasInstType}"></mwc-checkbox>
<mwc-checkbox
id="instType"
?checked="${content.hasInstType}"
></mwc-checkbox>
</mwc-formfield>`,
html`${pTypesGSESMV.map(
ptype =>
html`${Object.entries(content.attributes).map(
([key, value]) =>
html`<wizard-textfield
label="${ptype}"
.maybeValue=${parent
.querySelector(`Address > P[type="${ptype}"]`)
?.innerHTML.trim() ?? null}
?nullable=${typeNullable[ptype]}
pattern="${ifDefined(typePattern[ptype])}"
label="${key}"
?nullable=${typeNullable[key]}
.maybeValue=${value}
pattern="${ifDefined(typePattern[key])}"
required
></wizard-textfield>`
)}`,
Expand All @@ -56,25 +48,25 @@ function isEqualAddress(oldAddr: Element, newAdddr: Element): boolean {
);
}

function createAddressElement(
inputs: WizardInputElement[],
export function createAddressElement(
inputs: Record<string, string | null>,
parent: Element,
instType: boolean
): Element {
const element = createElement(parent.ownerDocument, 'Address', {});

inputs
.filter(input => getValue(input) !== null)
.forEach(validInput => {
const type = validInput.label;
Object.entries(inputs)
.filter(([key, value]) => value !== null)
.forEach(([key, value]) => {
const type = key;
const child = createElement(parent.ownerDocument, 'P', { type });
if (instType)
child.setAttributeNS(
'http://www.w3.org/2001/XMLSchema-instance',
'xsi:type',
'tP_' + type
'tP_' + key
);
child.textContent = getValue(validInput);
child.textContent = value;
element.appendChild(child);
});

Expand All @@ -83,7 +75,7 @@ function createAddressElement(

export function updateAddress(
parent: Element,
inputs: WizardInputElement[],
inputs: Record<string, string | null>,
instType: boolean
): (Create | Delete)[] {
const actions: (Create | Delete)[] = [];
Expand Down
68 changes: 68 additions & 0 deletions src/wizards/foundation/scl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
export function getConnectedAP(element: Element): Element | null {
return element.ownerDocument.querySelector(
`:root > Communication > SubNetwork > ConnectedAP[iedName="${element
.closest('IED')
?.getAttribute('name')}"][apName="${element
.closest('AccessPoint')
?.getAttribute('name')}"]`
);
}

export function isAccessPointConnected(element: Element): boolean {
return getConnectedAP(element) ? true : false;
}

function incrementMac(oldMac: string): string {
const mac = oldMac.split('-').join('');
//destination MAC in IEC61850 always starts with 01:0C:CD:...
const newMac = '0' + (parseInt(mac, 16) + 1).toString(16).toUpperCase();
return newMac.match(/.{1,2}/g)!.join('-');
}

export function uniqueMacAddress(
doc: XMLDocument,
serviceType: 'SMV' | 'GOOSE'
): string | null {
const maxMac =
serviceType === 'GOOSE' ? '01-0C-CD-01-01-FF' : '01-0C-CD-04-01-FF';
const startMac =
serviceType === 'GOOSE' ? '01-0C-CD-01-00-00' : '01-0C-CD-04-00-00';

const givenMacs = Array.from(doc.querySelectorAll('Address > P'))
.filter(pElement => pElement.getAttribute('type') === 'MAC-Address')
.map(mac => mac.innerHTML.trim());

let mac = startMac;
while (mac !== maxMac) {
if (!givenMacs.includes(mac)) return mac;
mac = incrementMac(mac);
}

return givenMacs.includes(mac) ? null : mac;
}

function incrementAppId(oldAppId: string): string {
return (parseInt(oldAppId, 16) + 1)
.toString(16)
.toUpperCase()
.padStart(4, '0');
}

export function uniqueAppId(doc: XMLDocument): string | null {
const maxAppId = 'FFFF';
const startAppId = '0001';

const givenAppIds = Array.from(doc.querySelectorAll('Address > P'))
.filter(pElement => pElement.getAttribute('type') === 'APPID')
.map(mac => mac.innerHTML.trim());

if (givenAppIds.length === 0) return null;

let appId = startAppId;
while (appId !== maxAppId) {
if (!givenAppIds.includes(appId)) return appId;
appId = incrementAppId(appId);
}

return givenAppIds.includes(appId) ? null : appId;
}
32 changes: 29 additions & 3 deletions src/wizards/gse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
WizardActor,
WizardInputElement,
} from '../foundation.js';
import { renderGseSmvAddress, updateAddress } from './address.js';
import { contentGseWizard, updateAddress } from './address.js';

export function getMTimeAction(
type: 'MinTime' | 'MaxTime',
Expand Down Expand Up @@ -67,7 +67,20 @@ export function updateGSEAction(element: Element): WizardActor {
const instType: boolean =
(<Checkbox>wizard.shadowRoot?.querySelector('#instType'))?.checked ??
false;
const addressActions = updateAddress(element, inputs, instType);

const addressContent: Record<string, string | null> = {};
addressContent['MAC-Address'] = getValue(
inputs.find(i => i.label === 'MAC-Address')!
);
addressContent['APPID'] = getValue(inputs.find(i => i.label === 'APPID')!);
addressContent['VLAN-ID'] = getValue(
inputs.find(i => i.label === 'VLAN-ID')!
);
addressContent['VLAN-PRIORITY'] = getValue(
inputs.find(i => i.label === 'VLAN-PRIORITY')!
);

const addressActions = updateAddress(element, addressContent, instType);

addressActions.forEach(action => {
complexAction.actions.push(action);
Expand Down Expand Up @@ -110,6 +123,19 @@ export function editGseWizard(element: Element): Wizard {
const minTime = element.querySelector('MinTime')?.innerHTML.trim() ?? null;
const maxTime = element.querySelector('MaxTime')?.innerHTML.trim() ?? null;

const hasInstType = Array.from(element.querySelectorAll('Address > P')).some(
pType => pType.getAttribute('xsi:type')
);

const attributes: Record<string, string | null> = {};

['MAC-Address', 'APPID', 'VLAN-ID', 'VLAN-PRIORITY'].forEach(key => {
if (!attributes[key])
attributes[key] =
element.querySelector(`Address > P[type="${key}"]`)?.innerHTML.trim() ??
null;
});

return [
{
title: get('wizard.title.edit', { tagName: element.tagName }),
Expand All @@ -120,7 +146,7 @@ export function editGseWizard(element: Element): Wizard {
action: updateGSEAction(element),
},
content: [
...renderGseSmvAddress(element),
...contentGseWizard({ hasInstType, attributes }),
html`<wizard-textfield
label="MinTime"
.maybeValue=${minTime}
Expand Down
Loading

0 comments on commit 887f46f

Please sign in to comment.