diff --git a/src/foundation/scl.ts b/src/foundation/scl.ts new file mode 100644 index 0000000000..966d47eadd --- /dev/null +++ b/src/foundation/scl.ts @@ -0,0 +1,89 @@ +import { crossProduct } from '../foundation.js'; + +function getDataModelChildren(parent: Element): Element[] { + if (['LDevice', 'Server'].includes(parent.tagName)) + return Array.from(parent.children).filter( + child => + child.tagName === 'LDevice' || + child.tagName === 'LN0' || + child.tagName === 'LN' + ); + + const id = + parent.tagName === 'LN' || parent.tagName === 'LN0' + ? parent.getAttribute('lnType') + : parent.getAttribute('type'); + + return Array.from( + parent.ownerDocument.querySelectorAll( + `LNodeType[id="${id}"] > DO, DOType[id="${id}"] > SDO, DOType[id="${id}"] > DA, DAType[id="${id}"] > BDA` + ) + ); +} + +export function existFcdaReference(fcda: Element, ied: Element): boolean { + const [ldInst, prefix, lnClass, lnInst, doName, daName, fc] = [ + 'ldInst', + 'prefix', + 'lnClass', + 'lnInst', + 'doName', + 'daName', + 'fc', + ].map(attr => fcda.getAttribute(attr)); + + const sinkLdInst = ied.querySelector(`LDevice[inst="${ldInst}"]`); + if (!sinkLdInst) return false; + + const prefixSelctors = prefix + ? [`[prefix="${prefix}"]`] + : ['[prefix=""]', ':not([prefix])']; + const lnInstSelectors = lnInst + ? [`[inst="${lnInst}"]`] + : ['[inst=""]', ':not([inst])']; + + const anyLnSelector = crossProduct( + ['LN0', 'LN'], + prefixSelctors, + [`[lnClass="${lnClass}"]`], + lnInstSelectors + ) + .map(strings => strings.join('')) + .join(','); + + const sinkAnyLn = ied.querySelector(anyLnSelector); + if (!sinkAnyLn) return false; + + const doNames = doName?.split('.'); + if (!doNames) return false; + + let parent: Element | undefined = sinkAnyLn; + for (const doNameAttr of doNames) { + parent = getDataModelChildren(parent).find( + child => child.getAttribute('name') === doNameAttr + ); + if (!parent) return false; + } + + const daNames = daName?.split('.'); + const someFcInSink = getDataModelChildren(parent).some( + da => da.getAttribute('fc') === fc + ); + if (!daNames && someFcInSink) return true; + if (!daNames) return false; + + let sinkFc = ''; + for (const daNameAttr of daNames) { + parent = getDataModelChildren(parent).find( + child => child.getAttribute('name') === daNameAttr + ); + + if (parent?.getAttribute('fc')) sinkFc = parent.getAttribute('fc')!; + + if (!parent) return false; + } + + if (sinkFc !== fc) return false; + + return true; +} diff --git a/src/translations/de.ts b/src/translations/de.ts index 3310787e61..e0db2cd793 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -293,28 +293,28 @@ export const de: Translations = { subscription: { none: 'Keine Verbindung vorhanden', publisherGoose: { - title: 'GOOSE-Publizierer' + title: 'GOOSE-Publizierer', }, subscriberIed: { title: 'Verbunden mit {{ selected }}', subscribed: 'Verbunden', availableToSubscribe: 'Kann verbunden werden', partiallySubscribed: 'Teilweise verbunden', - noGooseMessageSelected: 'Keine GOOSE ausgewählt' - } + noGooseMessageSelected: 'Keine GOOSE ausgewählt', + }, }, sampledvalues: { none: 'Keine Verbindung vorhanden', sampledValuesList: { - title: 'Sampled Values' + title: 'Sampled Values', }, subscriberIed: { title: 'Verbunden mit {{ selected }}', subscribed: 'Verbunden', availableToSubscribe: 'Kann verbunden werden', partiallySubscribed: 'Teilweise verbunden', - noSampledValuesSelected: 'Keinen Kontrollblock ausgewählt' - } + noSampledValuesSelected: 'Keinen Kontrollblock ausgewählt', + }, }, 'enum-val': { wizard: { @@ -526,6 +526,16 @@ export const de: Translations = { remove: '{{type}} "{{name}}" and referenzierte Element von IED {{iedName}} entfernt', }, + hints: { + source: 'Quell-IED', + missingServer: 'Kein Server vorhanden', + exist: '{{type}} mit dem Namen {{name}} existiert', + noMatchingData: 'Keine Datenübereinstimmung', + valid: 'Kann kopiert werden', + }, + label: { + copy: 'Kopie in anderen IEDs ertellen', + }, }, add: 'Hinzufügen', new: 'Neu', diff --git a/src/translations/en.ts b/src/translations/en.ts index 3182a72387..322466ce5e 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -289,28 +289,28 @@ export const en = { subscription: { none: 'None', publisherGoose: { - title: 'GOOSE publisher' + title: 'GOOSE publisher', }, subscriberIed: { title: 'Subscriber of {{ selected }}', subscribed: 'Subscribed', availableToSubscribe: 'Available to subscribe', partiallySubscribed: 'Partially subscribed', - noGooseMessageSelected: 'No GOOSE message selected' - } + noGooseMessageSelected: 'No GOOSE message selected', + }, }, sampledvalues: { none: 'none', sampledValuesList: { - title: 'Sampled Values' + title: 'Sampled Values', }, subscriberIed: { title: 'Subscriber of {{ selected }}', subscribed: 'Subscribed', availableToSubscribe: 'Available to subscribe', partiallySubscribed: 'Partially subscribed', - noSampledValuesSelected: 'No control block selected' - } + noSampledValuesSelected: 'No control block selected', + }, }, 'enum-val': { wizard: { @@ -512,7 +512,8 @@ export const en = { unreferencedDataSets: { title: 'Unreferenced Datasets', deleteButton: 'Remove Selected Datasets', - tooltip: 'Datasets without a reference to an associated GOOSE, Log, Report or Sampled Value Control Block' + tooltip: + 'Datasets without a reference to an associated GOOSE, Log, Report or Sampled Value Control Block', }, }, controlblock: { @@ -521,6 +522,14 @@ export const en = { remove: 'Removed {{type}} "{{name}}" and its referenced elements from IED {{iedName}}', }, + hints: { + source: 'Source IED', + missingServer: 'Not A Server', + exist: '{{type}} with name {{name}} already exist', + noMatchingData: 'No matching data', + valid: 'Can be copied', + }, + label: { copy: 'Copy to other IEDs' }, }, add: 'Add', new: 'New', diff --git a/src/wizards/foundation/finder.ts b/src/wizards/foundation/finder.ts index 87eab531d0..b146e40398 100644 --- a/src/wizards/foundation/finder.ts +++ b/src/wizards/foundation/finder.ts @@ -48,6 +48,16 @@ export function iEDPicker(doc: XMLDocument): TemplateResult { >`; } +export function iEDsPicker(doc: XMLDocument): TemplateResult { + return html` path[path.length - 1]} + >`; +} + export function getDataModelChildren(parent: Element): Element[] { if (['LDevice', 'Server'].includes(parent.tagName)) return Array.from(parent.children).filter( diff --git a/src/wizards/reportcontrol.ts b/src/wizards/reportcontrol.ts index 65e3e31eb3..4b475342ab 100644 --- a/src/wizards/reportcontrol.ts +++ b/src/wizards/reportcontrol.ts @@ -3,6 +3,7 @@ import { get, translate } from 'lit-translate'; import '@material/mwc-button'; import '@material/mwc-list/mwc-list-item'; +import '@material/mwc-list/mwc-check-list-item'; import { List } from '@material/mwc-list'; import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; import { SingleSelectedEvent } from '@material/mwc-list/mwc-list-foundation'; @@ -32,6 +33,7 @@ import { WizardAction, MenuAction, } from '../foundation.js'; +import { FilteredList } from '../filtered-list.js'; import { FinderList } from '../finder-list.js'; import { dataAttributePicker, iEDPicker } from './foundation/finder.js'; import { maxLength, patterns } from './foundation/limits.js'; @@ -39,6 +41,7 @@ import { editDataSetWizard } from './dataset.js'; import { newFCDA } from './fcda.js'; import { contentOptFieldsWizard, editOptFieldsWizard } from './optfields.js'; import { contentTrgOpsWizard, editTrgOpsWizard } from './trgops.js'; +import { existFcdaReference } from '../foundation/scl.js'; interface ContentOptions { name: string | null; @@ -400,6 +403,147 @@ function getRptEnabledAction( }; } +function copyReportControlActions(element: Element): WizardActor { + return (_: WizardInputElement[], wizard: Element) => { + const doc = element.ownerDocument; + + const iedItems = ( + wizard.shadowRoot?.querySelector('filtered-list')?.selected + ); + + const complexActions: ComplexAction[] = []; + iedItems.forEach(iedItem => { + const ied = doc.querySelector(selector('IED', iedItem.value)); + if (!ied) return; + + const sinkLn0 = ied.querySelector('LN0'); + if (!sinkLn0) return []; + + const sourceDataSet = element.parentElement?.querySelector( + `DataSet[name="${element.getAttribute('datSet')}"]` + ); + if ( + sourceDataSet && + sinkLn0.querySelector( + `DataSet[name="${sourceDataSet!.getAttribute('name')}"]` + ) + ) + return []; + + if ( + sinkLn0.querySelector( + `ReportControl[name="${element.getAttribute('name')}"]` + ) + ) + return []; + + // clone DataSet and make sure that FCDA is valid in ied + const sinkDataSet = sourceDataSet?.cloneNode(true); + Array.from(sinkDataSet.querySelectorAll('FCDA')).forEach(fcda => { + if (!existFcdaReference(fcda, ied)) sinkDataSet.removeChild(fcda); + }); + if (sinkDataSet.children.length === 0) return []; // when no data left no copy needed + + const sinkReportControl = element.cloneNode(true); + + const source = element.closest('IED')?.getAttribute('name'); + const sink = ied.getAttribute('name'); + + complexActions.push({ + title: `ReportControl copied from ${source} to ${sink}`, + actions: [ + { new: { parent: sinkLn0, element: sinkDataSet } }, + { new: { parent: sinkLn0, element: sinkReportControl } }, + ], + }); + }); + + return complexActions; + }; +} + +function renderIedListItem(sourceCb: Element, ied: Element): TemplateResult { + const sourceDataSet = sourceCb.parentElement?.querySelector( + `DataSet[name="${sourceCb.getAttribute('datSet')}"]` + ); + + const isSourceIed = + sourceCb.closest('IED')?.getAttribute('name') === ied.getAttribute('name'); + const ln0 = ied.querySelector('AccessPoint > Server > LDevice > LN0'); + const hasCbNameConflict = ln0?.querySelector( + `ReportControl[name="${sourceCb.getAttribute('name')}"]` + ) + ? true + : false; + const hasDataSetConflict = ln0?.querySelector( + `DataSet[name="${sourceDataSet?.getAttribute('name')}"]` + ) + ? true + : false; + + // clone DataSet and make sure that FCDA is valid in ied + const sinkDataSet = sourceDataSet?.cloneNode(true); + Array.from(sinkDataSet.querySelectorAll('FCDA')).forEach(fcda => { + if (!existFcdaReference(fcda, ied)) sinkDataSet.removeChild(fcda); + }); + const hasDataMatch = sinkDataSet.children.length > 0; + + const primSpan = ied.getAttribute('name'); + let secondSpan = ''; + if (isSourceIed) secondSpan = get('controlblock.hints.source'); + else if (!ln0) secondSpan = get('controlblock.hints.missingServer'); + else if (hasDataSetConflict && !isSourceIed) + secondSpan = get('controlblock.hints.exist', { + type: 'RerportControl', + name: sourceCb.getAttribute('name')!, + }); + else if (hasCbNameConflict && !isSourceIed) + secondSpan = get('controlblock.hints.exist', { + type: 'DataSet', + name: sourceCb.getAttribute('name')!, + }); + else if (!hasDataMatch) secondSpan = get('controlblock.hints.noMatchingData'); + else secondSpan = get('controlBlock.hints.valid'); + + return html`${primSpan}${secondSpan}`; +} + +export function reportControlCopyToIedSelector(element: Element): Wizard { + return [ + { + title: get('report.wizard.location'), + primary: { + icon: 'save', + label: get('save'), + action: copyReportControlActions(element), + }, + content: [ + html`${Array.from(element.ownerDocument.querySelectorAll('IED')).map( + ied => renderIedListItem(element, ied) + )}`, + ], + }, + ]; +} + +function openIedsSelector(element: Element): WizardMenuActor { + return (): WizardAction[] => { + return [() => reportControlCopyToIedSelector(element)]; + }; +} + export function removeReportControl(element: Element): WizardMenuActor { return (): WizardAction[] => { const complexAction = removeReportControlAction(element); @@ -531,6 +675,12 @@ export function editReportControlWizard(element: Element): Wizard { action: openOptFieldsWizard(optFields), }); + menuActions.push({ + icon: 'copy', + label: get('controlblock.label.copy'), + action: openIedsSelector(element), + }); + return [ { title: get('wizard.title.edit', { tagName: element.tagName }), diff --git a/test/integration/wizards/reportcontrol-wizarding-editing.test.ts b/test/integration/wizards/reportcontrol-wizarding-editing.test.ts index 0ca7187828..8be891645e 100644 --- a/test/integration/wizards/reportcontrol-wizarding-editing.test.ts +++ b/test/integration/wizards/reportcontrol-wizarding-editing.test.ts @@ -9,10 +9,12 @@ import { FilteredList } from '../../../src/filtered-list.js'; import { WizardTextField } from '../../../src/wizard-textfield.js'; import { createReportControlWizard, + reportControlCopyToIedSelector, reportControlParentSelector, selectReportControlWizard, } from '../../../src/wizards/reportcontrol.js'; import { FinderList } from '../../../src/finder-list.js'; +import { CheckListItem } from '@material/mwc-list/mwc-check-list-item'; describe('Wizards for SCL element ReportControl', () => { let doc: XMLDocument; @@ -333,6 +335,25 @@ describe('Wizards for SCL element ReportControl', () => { ) ).to.not.exist; }); + + it('opens a IEDs selector wizard on copy to other IEDs meu action', async () => { + const copyMenuAction = ( + Array.from( + element.wizardUI.dialog!.querySelectorAll( + 'mwc-menu > mwc-list-item' + ) + ).find(item => item.innerHTML.includes(`[controlblock.label.copy]`)) + ); + await element.wizardUI.dialog?.requestUpdate(); + copyMenuAction.click(); + await new Promise(resolve => setTimeout(resolve, 100)); // await animation + + const iedsPicker = + element.wizardUI.dialog?.querySelector('filtered-list'); + + expect(iedsPicker).to.exist; + expect(iedsPicker!.multi).to.be.true; + }); }); describe('defines a selector wizard to select ReportControl parent', () => { @@ -390,6 +411,133 @@ describe('Wizards for SCL element ReportControl', () => { }); }); + describe('defines a selector wizard to select ReportControl copy to sink', () => { + let iedsPicker: FilteredList; + let listItem: CheckListItem; + + beforeEach(async () => { + const sourceReportControl = doc.querySelector( + 'IED[name="IED2"] ReportControl[name="ReportCb"]' + )!; + const wizard = reportControlCopyToIedSelector(sourceReportControl); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + iedsPicker = ( + element.wizardUI.dialog?.querySelector('filtered-list') + ); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('opens a potential list of sink IEDs for the copy operation', () => + expect(iedsPicker).to.exist); + + describe('with a sink IED not meeting any of the data references', () => { + beforeEach(async () => { + listItem = ( + iedsPicker.items.find(item => item.value.includes('IED4'))! + ); + await element.requestUpdate(); + }); + + it('disables the list item', () => expect(listItem.disabled).to.be.true); + + it('does not copy the control block ', async () => { + listItem.selected = true; + await listItem.requestUpdate(); + primaryAction.click(); + expect(doc.querySelector('IED[name="IED4"] ReportControl')).to.not + .exist; + }); + }); + + describe('with a sink IED meeting partially the data references', () => { + beforeEach(async () => { + listItem = ( + iedsPicker.items.find(item => item.value.includes('IED5'))! + ); + + await element.requestUpdate(); + await listItem.requestUpdate(); + }); + + it('list item is selectable', () => + expect(listItem.disabled).to.be.false); + + it('does copy the control block ', async () => { + listItem.selected = true; + await listItem.requestUpdate(); + primaryAction.click(); + + expect(doc.querySelector('IED[name="IED5"] ReportControl')).to.exist; + }); + + it('removes non referenced data from the DataSet the control block ', async () => { + listItem.selected = true; + await listItem.requestUpdate(); + primaryAction.click(); + + const rpControl = doc.querySelector('IED[name="IED5"] ReportControl')!; + const dataSet = doc.querySelector( + `IED[name="IED5"] DataSet[name="${rpControl.getAttribute('datSet')}"]` + ); + expect(dataSet).to.exist; + expect(dataSet!.children).to.have.lengthOf(2); + }); + }); + + describe('with a sink IED already containing ReportControl', () => { + beforeEach(async () => { + listItem = ( + iedsPicker.items.find(item => item.value.includes('IED4'))! + ); + + await element.requestUpdate(); + }); + + it('list item is disabled', () => expect(listItem.disabled).to.be.true); + + it('does not copy report control block nor DataSet ', async () => { + listItem.selected = true; + await listItem.requestUpdate(); + primaryAction.click(); + + const rpControl = doc.querySelector('IED[name="IED6"] ReportControl')!; + expect(rpControl.getAttribute('datSet')).to.not.exist; + + const dataSet = doc.querySelector(`IED[name="IED6"] DataSet`); + expect(dataSet).to.not.exist; + }); + }); + + describe('with a sink IED already containing DataSet', () => { + beforeEach(async () => { + listItem = ( + iedsPicker.items.find(item => item.value.includes('IED7'))! + ); + + await element.requestUpdate(); + }); + + it('does not copy report control block nor DataSet ', async () => { + listItem.selected = true; + await listItem.requestUpdate(); + primaryAction.click(); + + const rpControl = doc.querySelector('IED[name="IED7"] ReportControl')!; + expect(rpControl).to.not.exist; + + const dataSet = doc.querySelector(`IED[name="IED7"] DataSet`); + expect(dataSet?.children).to.have.lengthOf(3); + }); + }); + }); + describe('defines a create wizards that', () => { let primaryAction: HTMLElement; diff --git a/test/testfiles/foundation/sclbasics.scd b/test/testfiles/foundation/sclbasics.scd new file mode 100644 index 0000000000..af173598b2 --- /dev/null +++ b/test/testfiles/foundation/sclbasics.scd @@ -0,0 +1,1535 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1000 + + + direct-with-enhanced-security + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1000 + + + direct-with-enhanced-security + + + + + + + + + + + 6000 + + + direct-with-enhanced-security + + + + + + + + + status-only + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + + + sbo-with-enhanced-security + + + 30000 + + + 600 + + + + + + + + + + + + + + + + + + + + + + + + sbo-with-enhanced-security + + + 30000 + + + 600 + + + + + + + + + + + + status-only + + + + + + + + + + + + + + + + + + + + + + + + + + + 6000 + + + direct-with-enhanced-security + + + + + + + + + status-only + + + + + + + + + IEC 61850-7-4:2007B4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + + + + 1000 + + + direct-with-enhanced-security + + + + + + + + + + 1000 + + + direct-with-enhanced-security + + + + + + + + + + + + + + + + + + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + IEC 61850-8-1:2003 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.001 + + + 0 + + + + + 0.01 + + + 0 + + + + + A + + + + + A + + + + + Hz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + on + blocked + test + test/blocked + off + + + status-only + direct-with-normal-security + sbo-with-normal-security + direct-with-enhanced-security + sbo-with-enhanced-security + + + status-only + + + not-supported + bay-control + station-control + remote-control + automatic-bay + automatic-station + automatic-remote + maintenance + process + + + y + z + a + f + p + n + µ + m + c + d + + da + h + k + M + G + T + P + E + Z + Y + + + V + A + other + Synchrophasor + + + None + ANSI Extremely Inverse + ANSI Very Inverse + ANSI Normal Inverse + ANSI Moderate Inverse + ANSI Definite Time + Long-Time Extremely Inverse + Long-Time Very Inverse + Long-Time Inverse + IEC Normal Inverse + IEC Very Inverse + IEC Inverse + IEC Extremely Inverse + IEC Short-Time Inverse + IEC Long-Time Inverse + IEC Definite Tim + Reserved + + + unknown + forward + backward + both + + + fundamental + rms + absolute + + + reserved + January + February + March + April + May + June + July + August + September + October + November + December + + + Time + WeekDay + WeekOfYear + DayOfMonth + DayOfYear + + + pulse + persistent + persistent-feedback + + + Hour + Day + Week + Month + Year + + + Va + Vb + Vc + Aa + Ab + Ac + Vab + Vbc + Vca + Vother + Aother + Synchrophasor + + + unknown + forward + backward + + + A + B + C + Synchrophasor + + + normal + high + low + high-high + low-low + + + + m + kg + s + A + K + mol + cd + deg + rad + sr + Gy + Bq + °C + Sv + F + C + S + H + V + ohm + J + N + Hz + lx + Lm + Wb + T + W + Pa + + + m/s + m/s² + m³/s + m/m³ + M + kg/m³ + m²/s + W/m K + J/K + ppm + 1/s + rad/s + W/m² + J/m² + S/m + K/s + Pa/s + J/kg K + VA + Watts + VAr + phi + cos(phi) + Vs + + As + + A²t + VAh + Wh + VArh + V/Hz + Hz/s + char + char/s + kgm² + dB + J/Wh + W/s + l/s + dBm + h + min + Ohm/m + percent/s + + + operate-once + operate-many + + + pos-neg-zero + dir-quad-zero + + + unknown + critical + major + minor + warning + + + reserved + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + Sunday + + + Completed + Cancelled + New adjustments + Under way + + + PhaseA + PhaseB + PhaseAB + PhaseC + PhaseAC + PhaseBC + PhaseABC + None + + + Ready + InProgress + + Successful + WaitingForTrip + TripFromProtection + FaultDisappeared + WaitToComplete + CBclosed + CycleUnsuccessful + Unsuccessful + Aborted + NotReady + + + None + Open + Close-Open + Open-Close-Open + Close-Open-Close-Open + Open-Close-Open-Close-Open + more + + + MS + PER_CYCLE + CYCLE + DAY + WEEK + MONTH + YEAR + EXTERNAL + + + UNSPECIFIED + TRUE_RMS + PEAK_FUNDAMENTAL + RMS_FUNDAMENTAL + MIN + MAX + AVG + SDV + PREDICTION + RATE + P-CLASS + M-CLASS + DIFF + + + TOTAL + PERIOD + SLIDING + + + Unknown + SNTP + PTP + IRIG-B + Substation internal + + + InternalClock + LocalAreaClock + GlobalAreaClock + + + Locked + Unlocked10s + Unlocked100s + Unlocked1000s + UnlockedMoreThan1000s + + + NonDirectional + Forward + Reverse + + + Current + Breaker Status + Both current and breaker status + Other + + + PhaseAtoGround + PhaseBtoGround + PhaseCtoGround + PhaseAtoB + PhaseBtoC + PhaseCtoA + Others + + + At Start Moment + At Trip Moment + Peak Fault Value + + + Low pass + High pass + Bandpass + Bandstop + Deadband + + + Slow time delay + Fast time delay + Fast acting + Very fast acting + Not applicable / Unknown + Other + + + Ok + Warning + Alarm + + + 0.05 + 0.1 + 0.2 + 0.2S + 0.5 + 0.5S + 1 + 3 + 5 + + + 1 + 3 + 5 + 6 + 10 + + + Unknown + Normal Time + Last minute of the day has 61 seconds + Last minute of the day has 59 seconds + + + Positive or Rising + Negative or Falling + Both + Other + + + Dead Line, Dead Bus + Live Line, Dead Bus + Dead Line, Live Bus + Dead Line, Dead Bus OR Live Line, Dead Bus + Dead Line, Dead Bus OR Dead Line, Live Bus + Live Line, Dead Bus OR Dead Line, Live Bus + Dead Line, Dead Bus OR Live Line, Dead Bus OR Dead Line, Live Bus + + + Air + Water + Steam + Oil + Hydrogen + Natural gas + Butane + Propane + Waste gas + Not applicable / Unknown + Other + + + Gaseous + Liquid + Solid + Not applicable / Unknown + Other + + + IEC + EEI + + + P + I + D + PI + PD + ID + PID + + + None + Close + Open + Close and Open + + + Master/Slave + Master/Slave with fixed slave position + Master/Slave with variable slave position + Parallel operation without communication + + + Master + Slave + Independent + + + No Mode predefined + Master + Follower + Power Factor + Negative Reactance + Circulating Current + Circulating Reactive Current (var balancing) + Circulating Reactive Current by equalizing power factor + + + None + Zero Sequence Current + Zero Sequence Voltage + Negative Sequence Voltage + Phase to Phase Voltages + Phase to Ground Voltages + Positive sequence voltage + + + Overwrite existing values + Stop when full or saturated + + + Current + Voltage + Active Power + + + None + Definite Time Delayed Reset + Inverse Reset + + + None + Harmonic2 + Harmonic5 + Harmonic2and5 + WaveformAnalysis + WaveformAnalysisAndHarmonic2 + Other + WaveformAnalysisAndHarmonic5 + WaveformAnalysisAndHarmonic2AndHarmonic5 + + + Off + Without Check + With Current Check + With Breaker Status Check + With Current and Breaker Status Check + Other Checks + + + Stopped + Stopping + Started + Starting + Disabled + + + Clockwise + Counter-Clockwise + Unknown + + + Cold + Warm + Overload + + + SwitchCommand + BreakerClosed + VoltageAndCurrentLevel + + + ExternalSignal + VoltageAndCurrent + ExternalSignal or VoltageAndCurrent + + + Vector + Arithmetic + + + None + Missing valid NumEnt + Missing valid SchdIntv + Missing valid schedule values + Inconsistent values CDC + Missing valid StrTm + Other + + + Not ready + Start Time required + Ready + Running + + + Ended normally + Ended with overshoot + Cancelled: measurement was deviating + Cancelled: loss of communication with dispatch centre + Cancelled: loss of communication with local area network + Cancelled: loss of communication with the local interface + Cancelled: timeout + Cancelled: voluntarily + Cancelled: noisy environments + Cancelled: material failure + Cancelled: new set-point request + Cancelled: improper environment (blockage) + Cancelled: stability time was reached + Cancelled: immobilisation time was reached + Cancelled: equipment was in the wrong mode + Unknown causes + + + Inactive + Stage1 + Stage2 + Stage3 + + + Load Break + Disconnector + Earthing Switch + High Speed Earthing Switch + + + None + Open + Close + Open and Close + + + Automatic-synchronizing + Automatic-paralleling + Manual + Test + + + pressure only + level only + both pressure and level + + + Unknown + P + PR + PX + PXR + TPX + TPY + TPZ + TPE + + + Unused + Blocking + Permissive + Direct + Unblocking + Status + + + Internal + External + Both + + + Single Pole Tripping + Undefined + Three Pole Tripping + + + 3 phase tripping + 1 or 3 phase tripping + specific + 1 phase tripping + + + Not tuned + Tuned + Tuned but not compensated + Umax + Umax but not compensated + Umax but not compensated due to U continous limitation + + + Negative sequence + Zero sequence + Neg-pos sequence + Zero-pos sequence + Phase vector comparison + Others + + + Off + Permanent + Time window + + + Voltage + Voltage and Current + Voltage and Normally Open breaker contact + Voltage and Normally Closed breaker contact + Voltage and Normally Open and Normally Closed breaker contacts + Normally Open breaker contact + Normally Closed breaker contact + Both Normally Open and Normally Closed breaker contacts + + + Off + Operate + Echo + Echo and Operate + + + + diff --git a/test/testfiles/wizards/reportcontrol.scd b/test/testfiles/wizards/reportcontrol.scd index 865382103d..e00607e101 100644 --- a/test/testfiles/wizards/reportcontrol.scd +++ b/test/testfiles/wizards/reportcontrol.scd @@ -210,6 +210,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + status-only + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + status-only + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + status-only + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + status-only + + + + + + + status-only + + + + + + + @@ -307,9 +504,9 @@ - - - + + + diff --git a/test/unit/foundation/scl.test.ts b/test/unit/foundation/scl.test.ts new file mode 100644 index 0000000000..89b1e40238 --- /dev/null +++ b/test/unit/foundation/scl.test.ts @@ -0,0 +1,112 @@ +import { expect } from '@open-wc/testing'; + +import { existFcdaReference } from '../../../src/foundation/scl.js'; + +describe('Global SCL related functions including', () => { + let ied: Element; + let fcda1: Element; + let fcda2: Element; + + beforeEach(async () => { + const doc = await fetch('/test/testfiles/foundation/sclbasics.scd') + .then(response => response.text()) + .then(str => new DOMParser().parseFromString(str, 'application/xml')); + + ied = doc.querySelector('IED')!; + + fcda1 = doc.createElementNS(doc.documentElement.namespaceURI, 'FCDA'); + fcda1.setAttribute('ldInst', 'ldInst1'); + fcda1.setAttribute('prefix', 'my'); + fcda1.setAttribute('lnClass', 'MMXU'); + fcda1.setAttribute('lnInst', '1'); + fcda1.setAttribute('doName', 'A.phsA'); + fcda1.setAttribute('daName', 'cVal.mag.i'); + fcda1.setAttribute('fc', 'MX'); + + fcda2 = doc.createElementNS(doc.documentElement.namespaceURI, 'FCDA'); + fcda2.setAttribute('ldInst', 'ldInst1'); + fcda2.setAttribute('prefix', ''); + fcda2.setAttribute('lnClass', 'LLN0'); + fcda2.setAttribute('lnInst', ''); + fcda2.setAttribute('doName', 'Beh'); + fcda2.setAttribute('daName', 'stVal'); + fcda2.setAttribute('fc', 'ST'); + }); + + describe('a fnuction that checks FCDA reference validity in IED that', () => { + it('return false for invalid LDevice instance', () => { + fcda1.setAttribute('ldInst', 'ldInst'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + }); + + it('returns false for invalid LN prefix', () => { + fcda1.setAttribute('prefix', 'mys'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + }); + + it('equally treats missing and empty LN prefix', () => { + fcda2.removeAttribute('prefix'); + expect(existFcdaReference(fcda2, ied)).to.be.true; + }); + + it('returns false for invalid LN lnClass', () => { + fcda1.setAttribute('lnClass', 'LLN0'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + }); + + it('returns false for invalid LN inst', () => { + fcda1.setAttribute('prefix', '2'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + }); + + it('equally treats missing and empty LN prefix', () => { + fcda2.removeAttribute('lnInst'); + expect(existFcdaReference(fcda2, ied)).to.be.true; + }); + + it('returns false for invalid DO name', () => { + fcda2.setAttribute('doName', 'beh'); + expect(existFcdaReference(fcda2, ied)).to.be.false; + }); + + it('returns false for invalid SDO name', () => { + fcda1.setAttribute('doName', 'A.PhsA'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + }); + + it('returns false for invalid DA name', () => { + fcda2.setAttribute('daName', 'StVal'); + expect(existFcdaReference(fcda2, ied)).to.be.false; + }); + + it('returns false for invalid BDA name', () => { + fcda1.setAttribute('daName', 'cVal.maG.i'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + }); + + it('returns false for missing FC in FCD type data reference', () => { + fcda1.removeAttribute('daName'); + fcda1.setAttribute('fc', 'CO'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + }); + + it('returns true for FCD type data reference', () => { + fcda1.removeAttribute('daName'); + expect(existFcdaReference(fcda1, ied)).to.be.true; + }); + + it('returns false for invalid FC definition', () => { + fcda1.setAttribute('fc', 'ST'); + expect(existFcdaReference(fcda1, ied)).to.be.false; + + fcda2.setAttribute('fc', 'MX'); + expect(existFcdaReference(fcda2, ied)).to.be.false; + }); + + it('returns true for existing MMXU data reference', () => + expect(existFcdaReference(fcda1, ied)).to.be.true); + + it('returns true for existing LLN0 data reference', () => + expect(existFcdaReference(fcda2, ied)).to.be.true); + }); +}); diff --git a/test/unit/wizards/__snapshots__/reportcontrol.test.snap.js b/test/unit/wizards/__snapshots__/reportcontrol.test.snap.js index 67898aebe5..af18ee8e66 100644 --- a/test/unit/wizards/__snapshots__/reportcontrol.test.snap.js +++ b/test/unit/wizards/__snapshots__/reportcontrol.test.snap.js @@ -71,6 +71,20 @@ snapshots["Wizards for SCL ReportControl element define an edit wizard that for edit + + + [controlblock.label.copy] + + + copy + +
@@ -185,6 +199,19 @@ snapshots["Wizards for SCL ReportControl element define an edit wizard that for delete + + + [controlblock.label.copy] + + + copy + +
@@ -353,6 +380,20 @@ snapshots["Wizards for SCL ReportControl element define a select wizard that wit IED3>>CBSW> XSWI 1>ReportCb2 + + + ReportCb + + + IED6>>CBSW>ReportCb + +
+
+ + + + IED2 + + + [controlblock.hints.source] + + + + + IED3 + + + [controlblock.hints.exist] + + + + + IED4 + + + [controlblock.hints.noMatchingData] + + + + + IED5 + + + [controlBlock.hints.valid] + + + + + IED6 + + + [controlblock.hints.exist] + + + + + IED7 + + + [controlblock.hints.exist] + + + +
+ + + + + +`; +/* end snapshot Wizards for SCL ReportControl element define copy to other IED selector looks like the latest snapshot */ + diff --git a/test/unit/wizards/reportcontrol.test.ts b/test/unit/wizards/reportcontrol.test.ts index 7ed64c733e..f3303de239 100644 --- a/test/unit/wizards/reportcontrol.test.ts +++ b/test/unit/wizards/reportcontrol.test.ts @@ -22,9 +22,12 @@ import { editReportControlWizard, removeReportControlAction, selectReportControlWizard, + reportControlCopyToIedSelector, } from '../../../src/wizards/reportcontrol.js'; import { inverseRegExp, regExp, regexString } from '../../foundation.js'; import { FinderList } from '../../../src/finder-list.js'; +import { FilteredList } from '../../../src/filtered-list.js'; +import { ListItemBase } from '@material/mwc-list/mwc-list-item-base'; describe('Wizards for SCL ReportControl element', () => { let doc: XMLDocument; @@ -687,4 +690,51 @@ describe('Wizards for SCL ReportControl element', () => { await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); }).timeout(5000); }); + + describe('define copy to other IED selector', () => { + let iedsPicker: FilteredList; + let listItem: ListItemBase; + + beforeEach(async () => { + const sourceReportControl = doc.querySelector( + 'IED[name="IED2"] ReportControl[name="ReportCb"]' + )!; + const wizard = reportControlCopyToIedSelector(sourceReportControl); + element.workflow.push(() => wizard); + await element.requestUpdate(); + + iedsPicker = ( + element.wizardUI.dialog?.querySelector('filtered-list') + ); + + primaryAction = ( + element.wizardUI.dialog?.querySelector( + 'mwc-button[slot="primaryAction"]' + ) + ); + }); + + it('looks like the latest snapshot', async () => { + await expect(element.wizardUI.dialog).dom.to.equalSnapshot(); + }).timeout(5000); + + it('allows to copy to multiple IED at once', () => + expect(iedsPicker.multi).to.be.true); + + describe('with a sink IED not meeting partially the data references', () => { + beforeEach(async () => { + listItem = iedsPicker.items.find(item => item.value.includes('IED5'))!; + await element.requestUpdate(); + }); + + it('disabled the list item', () => + expect(listItem.disabled).to.not.be.true); + + it('does copy the control block ', () => { + listItem.click(); + primaryAction.click(); + expect(actionEvent).to.have.been.called; + }); + }); + }); });