From 2066e4c71d8ab888cc04f1628c30b39582033fcb Mon Sep 17 00:00:00 2001 From: Steffen van den Driest <35229971+Stef3st@users.noreply.github.com> Date: Thu, 2 Nov 2023 14:11:29 +0100 Subject: [PATCH] fix: supervision updates after ied rename (#1338) * fix: supervision updates after ied rename Signed-off-by: Stef3st * chore: improve efficiency and tidy up Signed-off-by: Stef3st --------- Signed-off-by: Stef3st --- .../src/wizards/foundation/references.ts | 246 +++++++++++++----- .../open-scd/test/testfiles/wizards/ied.scd | 79 ++++++ .../wizards/foundation/references.test.ts | 66 ++++- 3 files changed, 315 insertions(+), 76 deletions(-) diff --git a/packages/open-scd/src/wizards/foundation/references.ts b/packages/open-scd/src/wizards/foundation/references.ts index e23a2019ff..d166d43871 100644 --- a/packages/open-scd/src/wizards/foundation/references.ts +++ b/packages/open-scd/src/wizards/foundation/references.ts @@ -2,13 +2,17 @@ import { Delete, getNameAttribute, isPublic, - Replace -} from "../../foundation.js"; + Replace, +} from '../../foundation.js'; const referenceInfoTags = ['IED', 'Substation', 'VoltageLevel', 'Bay'] as const; type ReferencesInfoTag = typeof referenceInfoTags[number]; -type FilterFunction = (element: Element, attributeName: string | null, oldName: string | null) => string; +type FilterFunction = ( + element: Element, + attributeName: string | null, + oldName: string | null +) => string; /* * For every supported tag a list of information about which elements to search for and which attribute value @@ -23,50 +27,68 @@ const referenceInfos: Record< filter: FilterFunction; }[] > = { - IED: - [{ + IED: [ + { attributeName: 'iedName', - filter: simpleAttributeFilter(`Association`) - }, { + filter: simpleAttributeFilter(`Association`), + }, + { attributeName: 'iedName', - filter: simpleAttributeFilter(`ClientLN`) - }, { + filter: simpleAttributeFilter(`ClientLN`), + }, + { attributeName: 'iedName', - filter: simpleAttributeFilter(`ConnectedAP`) - }, { + filter: simpleAttributeFilter(`ConnectedAP`), + }, + { attributeName: 'iedName', - filter: simpleAttributeFilter(`ExtRef`) - }, { + filter: simpleAttributeFilter(`ExtRef`), + }, + { attributeName: 'iedName', - filter: simpleAttributeFilter(`KDC`) - }, { + filter: simpleAttributeFilter(`KDC`), + }, + { attributeName: 'iedName', - filter: simpleAttributeFilter(`LNode`) - }, { + filter: simpleAttributeFilter(`LNode`), + }, + { attributeName: null, - filter: simpleTextContentFilter(`GSEControl > IEDName`) - }, { + filter: simpleTextContentFilter(`GSEControl > IEDName`), + }, + { attributeName: null, - filter: simpleTextContentFilter(`SampledValueControl > IEDName`) - }], - Substation: - [{ + filter: simpleTextContentFilter(`SampledValueControl > IEDName`), + }, + { + attributeName: null, + filter: simpleTextContentFilter(`LN > DOI > DAI > Val`), + }, + ], + Substation: [ + { attributeName: 'substationName', - filter: simpleAttributeFilter(`Terminal`) - }], - VoltageLevel: - [{ + filter: simpleAttributeFilter(`Terminal`), + }, + ], + VoltageLevel: [ + { attributeName: 'voltageLevelName', - filter: attributeFilterWithParentNameAttribute(`Terminal`, - {'Substation': 'substationName'}) - }], - Bay: - [{ + filter: attributeFilterWithParentNameAttribute(`Terminal`, { + Substation: 'substationName', + }), + }, + ], + Bay: [ + { attributeName: 'bayName', - filter: attributeFilterWithParentNameAttribute(`Terminal`, - {'Substation': 'substationName', 'VoltageLevel': 'voltageLevelName'}) - }], -} + filter: attributeFilterWithParentNameAttribute(`Terminal`, { + Substation: 'substationName', + VoltageLevel: 'voltageLevelName', + }), + }, + ], +}; /** * Simple function to create a filter to find Elements where the value of an attribute equals the old name. @@ -74,9 +96,13 @@ const referenceInfos: Record< * @param tagName - The tagName of the elements to search for. */ function simpleAttributeFilter(tagName: string) { - return function filter(element: Element, attributeName: string | null, oldName: string | null): string { + return function filter( + element: Element, + attributeName: string | null, + oldName: string | null + ): string { return `${tagName}[${attributeName}="${oldName}"]`; - } + }; } /** @@ -88,7 +114,7 @@ function simpleAttributeFilter(tagName: string) { function simpleTextContentFilter(elementQuery: string) { return function filter(): string { return `${elementQuery}`; - } + }; } /** @@ -105,19 +131,28 @@ function simpleTextContentFilter(elementQuery: string) { * @param parentInfo - The records of parent to search for, the key is the tagName of the parent, the value * is the name of the attribuet to use in the query. */ -function attributeFilterWithParentNameAttribute(tagName: string, parentInfo: Record) { - return function filter(element: Element, attributeName: string | null, oldName: string | null): string { - return `${tagName}${Object.entries(parentInfo) - .map(([parentTag, parentAttribute]) => { - const parentElement = element.closest(parentTag); - if (parentElement && parentElement.hasAttribute('name')) { - const name = parentElement.getAttribute('name'); - return `[${parentAttribute}="${name}"]`; - } - return null; - }).join('') // Join the strings to 1 string without a separator. +function attributeFilterWithParentNameAttribute( + tagName: string, + parentInfo: Record +) { + return function filter( + element: Element, + attributeName: string | null, + oldName: string | null + ): string { + return `${tagName}${ + Object.entries(parentInfo) + .map(([parentTag, parentAttribute]) => { + const parentElement = element.closest(parentTag); + if (parentElement && parentElement.hasAttribute('name')) { + const name = parentElement.getAttribute('name'); + return `[${parentAttribute}="${name}"]`; + } + return null; + }) + .join('') // Join the strings to 1 string without a separator. }[${attributeName}="${oldName}"]`; - } + }; } /** @@ -129,7 +164,11 @@ function attributeFilterWithParentNameAttribute(tagName: string, parentInfo: Rec * @param value - The value to set on the cloned element or if null remove the attribute. * @returns Returns the cloned element. */ -function cloneElement(element: Element, attributeName: string, value: string): Element { +function cloneElement( + element: Element, + attributeName: string, + value: string +): Element { const newElement = element.cloneNode(false); newElement.setAttribute(attributeName, value); return newElement; @@ -142,7 +181,10 @@ function cloneElement(element: Element, attributeName: string, value: string): E * @param value - The value to set. * @returns Returns the cloned element. */ -function cloneElementAndTextContent(element: Element, value: string | null): Element { +function cloneElementAndTextContent( + element: Element, + value: string | null +): Element { const newElement = element.cloneNode(false); newElement.textContent = value; return newElement; @@ -160,7 +202,11 @@ function cloneElementAndTextContent(element: Element, value: string | null): Ele * @param newName - The new name of the element. * @returns Returns a list of Replace Actions that can be added to a Complex Action or returned directly for execution. */ -export function updateReferences(element: Element, oldName: string | null, newName: string): Replace[] { +export function updateReferences( + element: Element, + oldName: string | null, + newName: string +): Replace[] { if (oldName === null || oldName === newName) { return []; } @@ -179,9 +225,13 @@ export function updateReferences(element: Element, oldName: string | null, newNa Array.from(element.ownerDocument.querySelectorAll(`${filter}`)) .filter(isPublic) .forEach(element => { - const newElement = cloneElement(element, info.attributeName!, newName); - actions.push({old: {element}, new: {element: newElement}}); - }) + const newElement = cloneElement( + element, + info.attributeName!, + newName + ); + actions.push({ old: { element }, new: { element: newElement } }); + }); } else { // If the text content needs to be updated, filter on the text content can't be done in a CSS Selector. // So we query all elements the may need to be updated and filter them afterwards. @@ -190,10 +240,71 @@ export function updateReferences(element: Element, oldName: string | null, newNa .filter(isPublic) .forEach(element => { const newElement = cloneElementAndTextContent(element, newName); - actions.push({old: {element}, new: {element: newElement}}); - }) + actions.push({ old: { element }, new: { element: newElement } }); + }); } - }) + }); + + if (element.tagName === 'IED') + actions.push(...updateVals(element, oldName, newName)); + return actions; +} + +/** + * Adds Replace actions to update supervision references. + * Only a maximum of one Val element per IED with ExtRef elements that contain src attributes will be altered. + * The Val element that needs to be altered will be found by checking if the controlBlockReference complies with this element. + * The controlBlockReference needs to contain the IED that gets renamed. + * + * @param element - The element for which the name is updated. + * @param oldName - The old name of the element. + * @param newName - The new name of the element. + */ +function updateVals(element: Element, oldName: string | null, newName: string) { + const actions: Replace[] = []; + const ieds = element.ownerDocument.querySelectorAll('IED'); + ieds.forEach(ied => { + // All Val elements inside LGOS and LSVS lnClasses that starts with the IED name that needs to be changed will be gathered. + // Because of a very rare case where multiple IED start with the same name, all will be gathered. + // If none are found continue to the next IED. + const valValues: Element[] = Array.from( + ied.querySelectorAll( + `:scope > AccessPoint > Server > LDevice > LN[lnClass="LGOS"] > DOI > DAI > Val, :scope > AccessPoint > Server > LDevice > LN[lnClass="LSVS"] > DOI > DAI > Val` + ) + ); + + if (valValues.length === 0) return; + + // If atleast one extRef element contains the to-be-changed IED name and has a srcCBName, one will be gathered. + // From that extRef element a controlblockreferences will be created and compared to the Val elements. + // If a match is found, the name of that Val element will be changed accordingly and the loop will be broken, as only 1 Val element need to be changed. + + const ref = ied.querySelector( + `:scope > AccessPoint > Server > LDevice > LN0 > Inputs > ExtRef[iedName="${oldName}"][srcCBName]` + ); + + const suffixCBReference = + ref?.getAttribute('srcLDInst') + + '/' + + ref?.getAttribute('srcLNClass') + + '.' + + ref?.getAttribute('srcCBName'); + + for (let value of valValues) { + if (oldName + suffixCBReference === value.textContent!.trim()) { + const newElement = cloneElementAndTextContent( + value, + newName + suffixCBReference + ); + actions.push({ + old: { element: value }, + new: { element: newElement }, + }); + break; + } + } + }); + return actions; } @@ -225,8 +336,8 @@ export function deleteReferences(element: Element): Delete[] { Array.from(element.ownerDocument.querySelectorAll(`${filter}`)) .filter(isPublic) .forEach(element => { - actions.push({old: { parent: element.parentElement!, element }}); - }) + actions.push({ old: { parent: element.parentElement!, element } }); + }); } else { // If the text content needs to be used for filtering, filter on the text content can't be done in a CSS Selector. // So we query all elements the may need to be deleted and filter them afterwards. @@ -236,10 +347,15 @@ export function deleteReferences(element: Element): Delete[] { .forEach(element => { // We not only need to remove the element containing the text content, but the parent of this element. if (element.parentElement) { - actions.push({old: {parent: element.parentElement.parentElement!, element: element.parentElement}}); + actions.push({ + old: { + parent: element.parentElement.parentElement!, + element: element.parentElement, + }, + }); } - }) + }); } - }) + }); return actions; } diff --git a/packages/open-scd/test/testfiles/wizards/ied.scd b/packages/open-scd/test/testfiles/wizards/ied.scd index 74c2b87215..753b1c2131 100644 --- a/packages/open-scd/test/testfiles/wizards/ied.scd +++ b/packages/open-scd/test/testfiles/wizards/ied.scd @@ -416,6 +416,85 @@ + + + + + + + + + + + + + + Publisher/LLN0.cb1 + + + + + + + + + + + + + + + + + + + + + + + Publisher/LLN0.cb1 + + + + + + + Publisher/LLN0.cb1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/open-scd/test/unit/wizards/foundation/references.test.ts b/packages/open-scd/test/unit/wizards/foundation/references.test.ts index fa80c1683e..3f8c1f8c3a 100644 --- a/packages/open-scd/test/unit/wizards/foundation/references.test.ts +++ b/packages/open-scd/test/unit/wizards/foundation/references.test.ts @@ -6,7 +6,7 @@ import { } from '../test-support.js'; import { deleteReferences, - updateReferences + updateReferences, } from '../../../../src/wizards/foundation/references.js'; import { expect } from '@open-wc/testing'; @@ -19,11 +19,17 @@ describe('Update reference for ', () => { beforeEach(async () => { doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); - conductingEquipment = doc.querySelector(`ConductingEquipment[name="${ceName}"]`)!; + conductingEquipment = doc.querySelector( + `ConductingEquipment[name="${ceName}"]` + )!; }); it('will update no references to ConductingEquipment', function () { - const updateActions = updateReferences(conductingEquipment, ceName, 'Other CE Name'); + const updateActions = updateReferences( + conductingEquipment, + ceName, + 'Other CE Name' + ); expect(updateActions.length).to.equal(0); }); @@ -38,7 +44,9 @@ describe('Update reference for ', () => { beforeEach(async () => { doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); - connectAP = doc.querySelector(`ConnectedAP[iedName="IED1"][apName="P1"]`)!; + connectAP = doc.querySelector( + `ConnectedAP[iedName="IED1"][apName="P1"]` + )!; }); it('will update no references to ConnectedAP', function () { @@ -52,6 +60,12 @@ describe('Update reference for ', () => { }); }); + describe('IED update Val element', () => { + beforeEach(async () => { + doc = await fetchDoc('/test/testfiles/wizards/iedRename.scd'); + }); + }); + describe('IED', () => { beforeEach(async () => { doc = await fetchDoc('/test/testfiles/wizards/ied.scd'); @@ -89,12 +103,7 @@ describe('Update reference for ', () => { const updateActions = updateReferences(ied, oldName, newName); expect(updateActions.length).to.equal(8); - expectUpdateTextValue( - updateActions[6], - 'GSEControl', - oldName, - newName - ); + expectUpdateTextValue(updateActions[6], 'GSEControl', oldName, newName); expectUpdateTextValue( updateActions[7], 'SampledValueControl', @@ -103,6 +112,39 @@ describe('Update reference for ', () => { ); }); + it('will update all references to IED Pub and checks for correct Val elements', function () { + const oldName = 'Pub'; + const newName = 'NewPub'; + const ied = doc.querySelector(`IED[name="${oldName}"]`)!; + + const updateActions = updateReferences(ied, oldName, newName); + expect(updateActions.length).to.equal(5); + + const input1 = updateActions[0].old.element; + const input2 = updateActions[1].old.element; + const input3 = updateActions[2].old.element; + const input4 = updateActions[3].old.element; + + expect(input1.getAttribute('srcCBName')).to.be.equal(null); + expect(input2.getAttribute('srcCBName')).to.be.equal(null); + expect(input3.getAttribute('srcCBName')).to.be.equal('cb1'); + expect(input4.getAttribute('srcCBName')).to.be.equal('cb1'); + + const valSuffix3 = + input3.getAttribute('srcLDInst') + + '/' + + input3.getAttribute('srcLNClass') + + '.' + + input3.getAttribute('srcCBName'); + + expectUpdateTextValue( + updateActions[4], + 'DAI', + oldName + valSuffix3, + newName + valSuffix3 + ); + }); + it('will delete all references to IED IED1', function () { const name = 'IED1'; const ied = doc.querySelector(`IED[name="${name}"]`)!; @@ -157,7 +199,9 @@ describe('Update reference for ', () => { it('will update all references to VoltageLevel "J1"', function () { const oldName = 'J1'; const newName = 'J1 UPD'; - const voltageLevel = doc.querySelector(`VoltageLevel[name="${oldName}"]`)!; + const voltageLevel = doc.querySelector( + `VoltageLevel[name="${oldName}"]` + )!; const updateActions = updateReferences(voltageLevel, oldName, newName); expect(updateActions.length).to.equal(48);