diff --git a/index.ts b/index.ts
index 9bee4a6..6ec9664 100644
--- a/index.ts
+++ b/index.ts
@@ -1,2 +1,3 @@
export { getReference } from './utils/tags.js';
export { identity } from './utils/identity.js';
+export { selector } from './utils/selector.js';
diff --git a/utils/edgeScl.ts b/utils/edgeScl.ts
new file mode 100644
index 0000000..1554a02
--- /dev/null
+++ b/utils/edgeScl.ts
@@ -0,0 +1,470 @@
+export const edgeScl = `
+
+
+ TrainingIEC61850
+
+
+
+
+
+
+
+
+
+ 100.0
+
+
+ 192.168.210.111
+ 255.255.255.0
+ 192.168.210.1
+ 1,3,9999,23
+ 23
+ 00000001
+ 0001
+ 0001
+
+
+
+ 01-0C-CD-01-00-10
+ 005
+ 4
+ 0010
+
+
+
+ RJ45
+
+
+ RJ45
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ textContent
+ IED2
+ IED2
+ IED3
+ IED4
+ IED1
+ IED2
+ IED3
+ IED5
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+ sbo-with-enhanced-security
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+ 1
+
+ 1
+ 2
+ 3
+
+
+
+ sbo-with-enhanced-security
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ status-only
+
+
+
+
+
+
+
+
+ direct-with-normal-security
+
+
+
+
+
+
+ sbo-with-normal-security
+ s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sbo-with-enhanced-security
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IEC 61850-8-1:2003
+
+
+
+
+
+
+
+ IEC 61850-8-1:2003
+
+
+
+
+
+
+
+
+ IEC 61850-8-2:2021
+
+
+
+
+
+
+
+
+
+ status-only
+ direct-with-normal-security
+ sbo-with-normal-security
+ direct-with-enhanced-security
+ sbo-with-enhanced-security
+
+
+ on
+ blocked
+ test
+ test/blocked
+ off
+
+
+ Ok
+ Warning
+ Alarm
+
+
+ not-supported
+ bay-control
+ station-control
+ remote-control
+ automatic-bay
+ automatic-station
+ automatic-remote
+ maintenance
+ process
+
+
+`;
diff --git a/utils/find.spec.ts b/utils/find.spec.ts
new file mode 100644
index 0000000..f58176a
--- /dev/null
+++ b/utils/find.spec.ts
@@ -0,0 +1,59 @@
+import { expect } from '@open-wc/testing';
+
+import { validScl } from './validScl.js';
+
+import { find, isPublic, selectorTags } from './find.js';
+import { identity } from './identity.js';
+
+const doc = new DOMParser().parseFromString(validScl, 'application/xml')!;
+
+describe('Function returning query selector', () => {
+ it('returns negation pseudo-class for identity of type NaN', () =>
+ expect(find(doc, 'Association', NaN)).to.be.null);
+
+ it('returns tagName with non SCL tag', () =>
+ expect(find(doc, 'LNodeSpec', 'someLNodeSpec')).to.be.null);
+
+ it('returns null for invalid extRefIdentity with non SCL tag', () => {
+ expect(find(doc, 'ExtRef', '@intAddr5]')).to.be.null;
+ expect(find(doc, 'ExtRef', ':GCB CBSW/ LLN0 / 2 stVal')).to.be.null;
+ expect(find(doc, 'Substation', 'IED1')).to.be.null;
+ });
+
+ it('returns null for invalid iEDNameIdentity with non SCL tag', () =>
+ expect(find(doc, 'IEDName', '@invalidId]')).to.be.null);
+
+ it('returns null for invalid pIdentity with non SCL tag', () => {
+ expect(find(doc, 'P', '@invalidId')).to.be.null;
+ expect(find(doc, 'P', 'IED1 P1> [0]')).to.be.null;
+ });
+
+ it('returns null for invalid protNsIdentity with non SCL tag', () =>
+ expect(find(doc, 'ProtNs', '@invalidId')).to.be.null);
+
+ it('returns null for invalid valIdentity with non SCL tag', () =>
+ expect(find(doc, 'Val', '@invalidId')).to.be.null);
+
+ it('returns null for LNode identity without lnType', () =>
+ expect(find(doc, 'LNode', '(IED1)')).to.be.null);
+
+ it('returns null for LNode identity without lnType', () => {
+ expect(find(doc, 'DAI', 'stVal')).to.be.null;
+ });
+
+ it('returns correct element for all tags', () => {
+ Object.keys(selectorTags).forEach(tag => {
+ const elements = Array.from(doc.querySelectorAll(tag)).filter(
+ item => !item.closest('Private')
+ );
+
+ elements.forEach(element => {
+ if (element && isPublic(element))
+ expect(element).to.satisfy(
+ // eslint-disable-next-line no-shadow
+ (element: Element) => element === find(doc, tag, identity(element))
+ );
+ });
+ });
+ });
+});
diff --git a/utils/find.ts b/utils/find.ts
new file mode 100644
index 0000000..95b55ac
--- /dev/null
+++ b/utils/find.ts
@@ -0,0 +1,873 @@
+/* eslint-disable no-use-before-define */
+import { isSCLTag, parentTags, SCLTag } from './tags.js';
+
+type IndexedSCLTags = 'ExtRef' | 'IEDName' | 'P' | 'ProtNs' | 'Val';
+
+export const voidSelector = ':not(*)';
+
+function crossProduct(...arrays: T[][]): T[][] {
+ return arrays.reduce(
+ (a, b) => a.flatMap(d => b.map(e => [d, e].flat())),
+ [[]]
+ );
+}
+
+function pathParts(identity: string): [string, string] {
+ const path = identity.split('>');
+ const end = path.pop() ?? '';
+ const start = path.join('>');
+ return [start, end];
+}
+
+function hitemSelector(tagName: SCLTag, identity: string): string {
+ const [version, revision] = identity.split('\t');
+
+ if (!version || !revision) return voidSelector;
+
+ return `${tagName}[version="${version}"][revision="${revision}"]`;
+}
+
+function terminalSelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, connectivityNode] = pathParts(identity);
+
+ const parentSelectors = parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ );
+
+ return crossProduct(
+ parentSelectors,
+ ['>'],
+ [`${tagName}[connectivityNode="${connectivityNode}"]`]
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function lNodeSelector(tagName: SCLTag, identity: string): string {
+ if (identity.endsWith(')')) {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+ const [lnClass, lnType] = childIdentity
+ .substring(1, childIdentity.length - 1)
+ .split(' ');
+
+ if (!lnClass || !lnType) return voidSelector;
+
+ const parentSelectors = parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ );
+
+ return crossProduct(
+ parentSelectors,
+ ['>'],
+ [`${tagName}[iedName="None"][lnClass="${lnClass}"][lnType="${lnType}"]`]
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+ }
+
+ const [iedName, ldInst, prefix, lnClass, lnInst] = identity.split(/[ /]/);
+
+ if (!iedName || !ldInst || !lnClass) return voidSelector;
+
+ const [
+ iedNameSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ ] = [
+ [`[iedName="${iedName}"]`],
+ ldInst === '(Client)'
+ ? [':not([ldInst])', '[ldInst=""]']
+ : [`[ldInst="${ldInst}"]`],
+ prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'],
+ [`[lnClass="${lnClass}"]`],
+ lnInst ? [`[lnInst="${lnInst}"]`] : [':not([lnInst])', '[lnInst=""]'],
+ ];
+
+ return crossProduct(
+ [tagName],
+ iedNameSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function kDCSelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+ const [iedName, apName] = childIdentity.split(' ');
+ return `${selector(
+ 'IED',
+ parentIdentity
+ )}>${tagName}[iedName="${iedName}"][apName="${apName}"]`;
+}
+
+function associationSelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const [iedName, ldInst, prefix, lnClass, lnInst] =
+ childIdentity.split(/[ /]/);
+
+ const parentSelectors = parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ );
+
+ const [
+ iedNameSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ ] = [
+ [`[iedName="${iedName}"]`],
+ [`[ldInst="${ldInst}"]`],
+ prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'],
+ [`[lnClass="${lnClass}"]`],
+ lnInst ? [`[lnInst="${lnInst}"]`] : [':not([lnInst])', '[lnInst=""]'],
+ ];
+
+ return crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ iedNameSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function lDeviceSelector(tagName: SCLTag, identity: string): string {
+ const [iedName, inst] = identity.split('>>');
+
+ if (!inst) return voidSelector;
+
+ return `IED[name="${iedName}"] ${tagName}[inst="${inst}"]`;
+}
+
+function fCDASelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const [ldInst, prefix, lnClass, lnInst] = childIdentity.split(/[ /.]/);
+
+ const matchDoDa = childIdentity.match(
+ /.([A-Z][A-Za-z0-9.]*) ([A-Za-z0-9.]*) \(/
+ );
+ const doName = matchDoDa && matchDoDa[1] ? matchDoDa[1] : '';
+ const daName = matchDoDa && matchDoDa[2] ? matchDoDa[2] : '';
+
+ const matchFx = childIdentity.match(/\(([A-Z]{2})/);
+ const matchIx = childIdentity.match(/ \[([0-9]{1,2})\]/);
+
+ const fc = matchFx && matchFx[1] ? matchFx[1] : '';
+ const ix = matchIx && matchIx[1] ? matchIx[1] : '';
+
+ const [
+ parentSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ doNameSelectors,
+ daNameSelectors,
+ fcSelectors,
+ ixSelectors,
+ ] = [
+ parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ ),
+ [`[ldInst="${ldInst}"]`],
+ prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'],
+ [`[lnClass="${lnClass}"]`],
+ lnInst ? [`[lnInst="${lnInst}"]`] : [':not([lnInst])', '[lnInst=""]'],
+ [`[doName="${doName}"]`],
+ daName ? [`[daName="${daName}"]`] : [':not([daName])', '[daName=""]'],
+ [`[fc="${fc}"]`],
+ ix ? [`[ix="${ix}"]`] : [':not([ix])', '[ix=""]'],
+ ];
+
+ return crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ doNameSelectors,
+ daNameSelectors,
+ fcSelectors,
+ ixSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function lNSelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const parentSelectors = parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ );
+
+ const [prefix, lnClass, inst] = childIdentity.split(' ');
+
+ if (!lnClass) return voidSelector;
+
+ const [prefixSelectors, lnClassSelectors, instSelectors] = [
+ prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'],
+ [`[lnClass="${lnClass}"]`],
+ [`[inst="${inst}"]`],
+ ];
+
+ return crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ prefixSelectors,
+ lnClassSelectors,
+ instSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function clientLNSelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const parentSelectors = parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ );
+
+ const [iedName, apRef, ldInst, prefix, lnClass, lnInst] =
+ childIdentity.split(/[ /]/);
+
+ const [
+ iedNameSelectors,
+ apRefSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ ] = [
+ iedName ? [`[iedName="${iedName}"]`] : [':not([iedName])', '[iedName=""]'],
+ apRef ? [`[apRef="${apRef}"]`] : [':not([apRef])', '[apRef=""]'],
+ ldInst ? [`[ldInst="${ldInst}"]`] : [':not([ldInst])', '[ldInst=""]'],
+ prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'],
+ [`[lnClass="${lnClass}"]`],
+ lnInst ? [`[lnInst="${lnInst}"]`] : [':not([lnInst])', '[lnInst=""]'],
+ ];
+
+ return crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ iedNameSelectors,
+ apRefSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function ixNamingSelector(
+ tagName: SCLTag,
+ identity: string,
+ depth = -1
+): string {
+ // eslint-disable-next-line no-param-reassign
+ if (depth === -1) depth = identity.split('>').length;
+
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [_0, name, _1, ix] =
+ childIdentity.match(/([^[]*)(\[([0-9]*)\])?/) ?? [];
+
+ if (!name) return voidSelector;
+
+ const parentSelectors = parentTags(tagName)
+ .flatMap(parentTag =>
+ parentTag === 'SDI'
+ ? ixNamingSelector(parentTag, parentIdentity, depth - 1).split(',')
+ : selector(parentTag, parentIdentity).split(',')
+ )
+ // eslint-disable-next-line no-shadow
+ .filter(selector => !selector.startsWith(voidSelector));
+
+ if (parentSelectors.length === 0) return voidSelector;
+
+ const [nameSelectors, ixSelectors] = [
+ [`[name="${name}"]`],
+ ix ? [`[ix="${ix}"]`] : ['[ix=""]', ':not([ix])'],
+ ];
+
+ return crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ nameSelectors,
+ ixSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function connectedAPSelector(tagName: SCLTag, identity: string): string {
+ const [iedName, apName] = identity.split(' ');
+ if (!iedName || !apName) return voidSelector;
+ return `${tagName}[iedName="${iedName}"][apName="${apName}"]`;
+}
+
+function controlBlockSelector(tagName: SCLTag, identity: string): string {
+ const [ldInst, cbName] = identity.split(' ');
+
+ if (!ldInst || !cbName) return voidSelector;
+
+ return `${tagName}[ldInst="${ldInst}"][cbName="${cbName}"]`;
+}
+
+function physConnSelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, pcType] = pathParts(identity);
+
+ const [parentSelectors, typeSelectors] = [
+ parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ ),
+ pcType ? [`[type="${pcType}"]`] : [''],
+ ];
+
+ return crossProduct(parentSelectors, ['>'], [tagName], typeSelectors)
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function enumValSelector(tagName: SCLTag, identity: string): string {
+ const [parentIdentity, ord] = pathParts(identity);
+ return `${selector('EnumType', parentIdentity)}>${tagName}[ord="${ord}"]`;
+}
+
+function sCLSelector(): string {
+ return ':root';
+}
+
+function namingSelector(tagName: SCLTag, identity: string, depth = -1): string {
+ // eslint-disable-next-line no-param-reassign
+ if (depth === -1) depth = identity.split('>').length;
+
+ const [parentIdentity, name] = pathParts(identity);
+ if (!name) return voidSelector;
+
+ // eslint-disable-next-line prefer-destructuring
+ const parents = parentTags(tagName) as SCLTag[];
+
+ const parentSelectors = parents
+ .flatMap(parentTag =>
+ selectorTags[parentTag] === selectorTags.Substation
+ ? namingSelector(parentTag, parentIdentity, depth - 1).split(',')
+ : selector(parentTag, parentIdentity).split(',')
+ )
+ // eslint-disable-next-line no-shadow
+ .filter(selector => !selector.startsWith(voidSelector));
+
+ if (parentSelectors.length === 0) return voidSelector;
+
+ return crossProduct(parentSelectors, ['>'], [tagName], [`[name="${name}"]`])
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function singletonSelector(tagName: SCLTag, identity: string): string {
+ // eslint-disable-next-line prefer-destructuring
+ const parents = parentTags(tagName) as SCLTag[];
+
+ const parentSelectors = parents
+ .flatMap(parentTag => selector(parentTag, identity).split(','))
+ // eslint-disable-next-line no-shadow
+ .filter(selector => !selector.startsWith(voidSelector));
+
+ if (parentSelectors.length === 0) return voidSelector;
+
+ return crossProduct(parentSelectors, ['>'], [tagName])
+ .map(strings => strings.join(''))
+ .join(',');
+}
+
+function idNamingSelector(tagName: SCLTag, identity: string): string {
+ const id = identity.replace(/^#/, '');
+
+ if (!id) return voidSelector;
+
+ return `${tagName}[id="${id}"]`;
+}
+
+type SelectorFunction = (tagName: SCLTag, identity: string) => string;
+
+export const selectorTags: Record = {
+ AccessControl: singletonSelector,
+ AccessPoint: namingSelector,
+ Address: singletonSelector,
+ Association: associationSelector,
+ Authentication: singletonSelector,
+ BDA: namingSelector,
+ BitRate: singletonSelector,
+ Bay: namingSelector,
+ ClientLN: clientLNSelector,
+ ClientServices: singletonSelector,
+ CommProt: singletonSelector,
+ Communication: singletonSelector,
+ ConductingEquipment: namingSelector,
+ ConfDataSet: singletonSelector,
+ ConfLdName: singletonSelector,
+ ConfLNs: singletonSelector,
+ ConfLogControl: singletonSelector,
+ ConfReportControl: singletonSelector,
+ ConfSG: singletonSelector,
+ ConfSigRef: singletonSelector,
+ ConnectedAP: connectedAPSelector,
+ ConnectivityNode: namingSelector,
+ DA: namingSelector,
+ DAI: ixNamingSelector,
+ DAType: idNamingSelector,
+ DO: namingSelector,
+ DOI: namingSelector,
+ DOType: idNamingSelector,
+ DataObjectDirectory: singletonSelector,
+ DataSet: namingSelector,
+ DataSetDirectory: singletonSelector,
+ DataTypeTemplates: singletonSelector,
+ DynAssociation: singletonSelector,
+ DynDataSet: singletonSelector,
+ EnumType: idNamingSelector,
+ EnumVal: enumValSelector,
+ EqFunction: namingSelector,
+ EqSubFunction: namingSelector,
+ ExtRef: () => voidSelector,
+ FCDA: fCDASelector,
+ FileHandling: singletonSelector,
+ Function: namingSelector,
+ GeneralEquipment: namingSelector,
+ GetCBValues: singletonSelector,
+ GetDataObjectDefinition: singletonSelector,
+ GetDataSetValue: singletonSelector,
+ GetDirectory: singletonSelector,
+ GOOSE: singletonSelector,
+ GOOSESecurity: namingSelector,
+ GSE: controlBlockSelector,
+ GSEDir: singletonSelector,
+ GSEControl: namingSelector,
+ GSESettings: singletonSelector,
+ GSSE: singletonSelector,
+ Header: singletonSelector,
+ History: singletonSelector,
+ Hitem: hitemSelector,
+ IED: namingSelector,
+ IEDName: () => voidSelector,
+ Inputs: singletonSelector,
+ IssuerName: singletonSelector,
+ KDC: kDCSelector,
+ LDevice: lDeviceSelector,
+ LN: lNSelector,
+ LN0: singletonSelector,
+ LNode: lNodeSelector,
+ LNodeType: idNamingSelector,
+ Line: namingSelector,
+ Log: namingSelector,
+ LogControl: namingSelector,
+ LogSettings: singletonSelector,
+ MaxTime: singletonSelector,
+ McSecurity: singletonSelector,
+ MinTime: singletonSelector,
+ NeutralPoint: terminalSelector,
+ OptFields: singletonSelector,
+ P: () => voidSelector,
+ PhysConn: physConnSelector,
+ PowerTransformer: namingSelector,
+ Private: () => voidSelector,
+ Process: namingSelector,
+ ProtNs: () => voidSelector,
+ Protocol: singletonSelector,
+ ReadWrite: singletonSelector,
+ RedProt: singletonSelector,
+ ReportControl: namingSelector,
+ ReportSettings: singletonSelector,
+ RptEnabled: singletonSelector,
+ SamplesPerSec: singletonSelector,
+ SampledValueControl: namingSelector,
+ SecPerSamples: singletonSelector,
+ SCL: sCLSelector,
+ SDI: ixNamingSelector,
+ SDO: namingSelector,
+ Server: singletonSelector,
+ ServerAt: singletonSelector,
+ Services: singletonSelector,
+ SetDataSetValue: singletonSelector,
+ SettingControl: singletonSelector,
+ SettingGroups: singletonSelector,
+ SGEdit: singletonSelector,
+ SmpRate: singletonSelector,
+ SMV: controlBlockSelector,
+ SmvOpts: singletonSelector,
+ SMVsc: singletonSelector,
+ SMVSecurity: namingSelector,
+ SMVSettings: singletonSelector,
+ SubEquipment: namingSelector,
+ SubFunction: namingSelector,
+ SubNetwork: namingSelector,
+ Subject: singletonSelector,
+ Substation: namingSelector,
+ SupSubscription: singletonSelector,
+ TapChanger: namingSelector,
+ Terminal: terminalSelector,
+ Text: singletonSelector,
+ TimerActivatedControl: singletonSelector,
+ TimeSyncProt: singletonSelector,
+ TransformerWinding: namingSelector,
+ TrgOps: singletonSelector,
+ Val: () => voidSelector,
+ ValueHandling: singletonSelector,
+ Voltage: singletonSelector,
+ VoltageLevel: namingSelector,
+};
+
+function selector(tagName: SCLTag, identity: string | number): string {
+ if (typeof identity !== 'string') return voidSelector;
+
+ return selectorTags[tagName](tagName, identity);
+}
+
+function findExtRef(
+ root: XMLDocument | Element | DocumentFragment,
+ tagName: IndexedSCLTags,
+ identity: string
+): Element | null {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const parentSelectors = parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ );
+
+ if (childIdentity.endsWith(']')) {
+ const [intAddr] = childIdentity.split('[');
+ const intAddrSelectors = [`[intAddr="${intAddr}"]`];
+
+ const index =
+ childIdentity &&
+ childIdentity.match(/\[([0-9]+)\]/) &&
+ childIdentity.match(/\[([0-9]+)\]/)![1]
+ ? parseFloat(childIdentity.match(/\[([0-9]+)\]/)![1])
+ : NaN;
+
+ const extRefSelector = crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ intAddrSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+
+ return (
+ Array.from(root.querySelectorAll(extRefSelector)).filter(isPublic)[
+ index
+ ] ?? null
+ );
+ }
+
+ let iedName;
+ let ldInst;
+ let prefix;
+ let lnClass;
+ let lnInst;
+ let doName;
+ let daName;
+ let serviceType;
+ let srcCBName;
+ let srcLDInst;
+ let srcPrefix;
+ let srcLNClass;
+ let srcLNInst;
+
+ if (!childIdentity.includes(':') && !childIdentity.includes('@')) {
+ [iedName, ldInst, prefix, lnClass, lnInst, doName, daName] =
+ childIdentity.split(/[ /]/);
+ } else if (childIdentity.includes(':') && !childIdentity.includes('@')) {
+ [
+ serviceType,
+ srcCBName,
+ srcLDInst,
+ srcPrefix,
+ srcLNClass,
+ srcLNInst,
+ iedName,
+ ldInst,
+ prefix,
+ lnClass,
+ lnInst,
+ doName,
+ daName,
+ ] = childIdentity.split(/[ /:]/);
+ }
+
+ const [
+ iedNameSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ doNameSelectors,
+ daNameSelectors,
+ serviceTypeSelectors,
+ srcCBNameSelectors,
+ srcLDInstSelectors,
+ srcPrefixSelectors,
+ srcLNClassSelectors,
+ srcLNInstSelectors,
+ ] = [
+ iedName ? [`[iedName="${iedName}"]`] : [':not([iedName])'],
+ ldInst ? [`[ldInst="${ldInst}"]`] : [':not([ldInst])', '[ldInst=""]'],
+ prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'],
+ lnClass ? [`[lnClass="${lnClass}"]`] : [':not([lnClass])'],
+ lnInst ? [`[lnInst="${lnInst}"]`] : [':not([lnInst])', '[lnInst=""]'],
+ doName ? [`[doName="${doName}"]`] : [':not([doName])'],
+ daName ? [`[daName="${daName}"]`] : [':not([daName])', '[daName=""]'],
+ serviceType
+ ? [`[serviceType="${serviceType}"]`]
+ : [':not([serviceType])', '[serviceType=""]'],
+ srcCBName
+ ? [`[srcCBName="${srcCBName}"]`]
+ : [':not([srcCBName])', '[srcCBName=""]'],
+ srcLDInst
+ ? [`[srcLDInst="${srcLDInst}"]`]
+ : [':not([srcLDInst])', '[srcLDInst=""]'],
+ srcPrefix
+ ? [`[srcPrefix="${srcPrefix}"]`]
+ : [':not([srcPrefix])', '[srcPrefix=""]'],
+ srcLNClass
+ ? [`[srcLNClass="${srcLNClass}"]`]
+ : [':not([srcLNClass])', '[srcLNClass=""]'],
+ srcLNInst
+ ? [`[srcLNInst="${srcLNInst}"]`]
+ : [':not([srcLNInst])', '[srcLNInst=""]'],
+ ];
+
+ const extRefSelector = crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ iedNameSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ doNameSelectors,
+ daNameSelectors,
+ serviceTypeSelectors,
+ srcCBNameSelectors,
+ srcLDInstSelectors,
+ srcPrefixSelectors,
+ srcLNClassSelectors,
+ srcLNInstSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+
+ return (
+ Array.from(root.querySelectorAll(extRefSelector)).filter(isPublic)[0] ??
+ null
+ );
+}
+
+function findIEDName(
+ root: XMLDocument | Element | DocumentFragment,
+ tagName: IndexedSCLTags,
+ identity: string
+): Element | null {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const [iedName, apRef, ldInst, prefix, lnClass, lnInst] =
+ childIdentity.split(/[ /]/);
+
+ const [
+ parentSelectors,
+ apRefSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors,
+ ] = [
+ parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ ),
+ apRef ? [`[apRef="${apRef}"]`] : [':not([apRef])', '[apRef=""]'],
+ ldInst ? [`[ldInst="${ldInst}"]`] : [':not([ldInst])', '[ldInst=""]'],
+ prefix ? [`[prefix="${prefix}"]`] : [':not([prefix])', '[prefix=""]'],
+ lnClass ? [`[lnClass="${lnClass}"]`] : [':not([lnClass])', '[lnClass=""]'],
+ lnInst ? [`[lnInst="${lnInst}"]`] : [':not([lnInst])', '[lnInst=""]'],
+ ];
+
+ const iEDNameSelector = crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ apRefSelectors,
+ ldInstSelectors,
+ prefixSelectors,
+ lnClassSelectors,
+ lnInstSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+
+ return (
+ Array.from(root.querySelectorAll(iEDNameSelector))
+ .filter(isPublic)
+ .find(iEDName => iEDName.textContent === iedName) ?? null
+ );
+}
+
+function findP(
+ root: XMLDocument | Element | DocumentFragment,
+ tagName: IndexedSCLTags,
+ identity: string
+): Element | null {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const [type] = childIdentity.split(' ');
+ const index =
+ childIdentity &&
+ childIdentity.match(/\[([0-9]+)\]/) &&
+ childIdentity.match(/\[([0-9]+)\]/)![1]
+ ? parseFloat(childIdentity.match(/\[([0-9]+)\]/)![1])
+ : NaN;
+
+ const [parentSelectors, typeSelectors] = [
+ parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ ),
+ [`[type="${type}"]`],
+ ];
+
+ const pSelector = crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ typeSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+
+ return Number.isNaN(index)
+ ? Array.from(root.querySelectorAll(pSelector)).find(isPublic) ?? null
+ : Array.from(root.querySelectorAll(pSelector)).filter(isPublic)[index] ??
+ null;
+}
+
+function findProtNs(
+ root: XMLDocument | Element | DocumentFragment,
+ tagName: IndexedSCLTags,
+ identity: string
+): Element | null {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const [type, protNsContent] = childIdentity.split('\t');
+
+ const [parentSelectors, typeSelector] = [
+ parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ ),
+ type && type !== '8-MMS'
+ ? [`[type="${type}"]`]
+ : [':not([type])', '[type="8-MMS"]'],
+ ];
+
+ const protNsSelector = crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ typeSelector
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+
+ return (
+ Array.from(root.querySelectorAll(protNsSelector))
+ .filter(isPublic)
+ .find(protNs => protNs.textContent === protNsContent) ?? null
+ );
+}
+
+function findVal(
+ root: XMLDocument | Element | DocumentFragment,
+ tagName: IndexedSCLTags,
+ identity: string
+): Element | null {
+ const [parentIdentity, childIdentity] = pathParts(identity);
+
+ const [sGroup, indexText] = childIdentity.split(' ');
+ const index = parseFloat(indexText);
+
+ const parentSelectors = parentTags(tagName).flatMap(parentTag =>
+ selector(parentTag, parentIdentity).split(',')
+ );
+
+ const [nameSelectors] = [sGroup ? [`[sGroup="${sGroup}"]`] : ['']];
+
+ const valSelector = crossProduct(
+ parentSelectors,
+ ['>'],
+ [tagName],
+ nameSelectors
+ )
+ .map(strings => strings.join(''))
+ .join(',');
+
+ return (
+ Array.from(root.querySelectorAll(valSelector)).filter(isPublic)[index] ??
+ null
+ );
+}
+
+type FindFunction = (
+ doc: XMLDocument | Element | DocumentFragment,
+ tagName: 'ExtRef',
+ identity: string
+) => Element | null;
+
+const sclTags: Record = {
+ ExtRef: findExtRef,
+ IEDName: findIEDName,
+ P: findP,
+ ProtNs: findProtNs,
+ Val: findVal,
+};
+
+export function isPublic(element: Element): boolean {
+ return !element.closest('Private');
+}
+
+export function find(
+ root: XMLDocument | Element | DocumentFragment,
+ tagName: string,
+ identity: string | number
+): Element | null {
+ if (typeof identity !== 'string' || !isSCLTag(tagName)) return null;
+
+ if (sclTags[tagName]) return sclTags[tagName](root, tagName, identity);
+
+ return (
+ Array.from(
+ root.querySelectorAll(selectorTags[tagName](tagName, identity))
+ ).filter(isPublic)[0] ?? null
+ );
+}
diff --git a/utils/identity.spec.ts b/utils/identity.spec.ts
index 0e480b6..e906f94 100644
--- a/utils/identity.spec.ts
+++ b/utils/identity.spec.ts
@@ -1,9 +1,11 @@
import { expect } from '@open-wc/testing';
+import { edgeScl } from './edgeScl.js';
import { identity } from './identity.js';
import { validScl } from './validScl.js';
const scl = new DOMParser().parseFromString(validScl, 'application/xml');
+const sclEdge = new DOMParser().parseFromString(edgeScl, 'application/xml');
describe('identity', () => {
it('returns NaN for element null', () => expect(identity(null)).to.be.NaN);
@@ -14,6 +16,50 @@ describe('identity', () => {
it('returns NaN for any private element', () =>
expect(identity(scl.querySelector('Private'))).to.be.NaN);
+ it('returns NaN for extension type PhysConn element', () =>
+ expect(identity(sclEdge.querySelector('PhysConn[type="SomeOtherType"]'))).to
+ .be.NaN);
+
+ it('returns NaN with orphan ExtRef element', () => {
+ const extRef = new DOMParser()
+ .parseFromString(``, 'application/xml')
+ .querySelector('ExtRef')!;
+
+ expect(identity(extRef)).to.be.NaN;
+ });
+
+ it('returns NaN with orphan Val element', () => {
+ const val = new DOMParser()
+ .parseFromString(``, 'application/xml')
+ .querySelector('Val')!;
+
+ expect(identity(val)).to.be.NaN;
+ });
+
+ it('returns NaN with orphan PhysConn element', () => {
+ const val = new DOMParser()
+ .parseFromString(``, 'application/xml')
+ .querySelector('PhysConn')!;
+
+ expect(identity(val)).to.be.NaN;
+ });
+
+ it('returns NaN with orphan P element', () => {
+ const val = new DOMParser()
+ .parseFromString(``, 'application/xml')
+ .querySelector('P')!;
+
+ expect(identity(val)).to.be.NaN;
+ });
+
+ it('returns NaN with orphan ProtNs element', () => {
+ const val = new DOMParser()
+ .parseFromString(``, 'application/xml')
+ .querySelector('ProtNs')!;
+
+ expect(identity(val)).to.be.NaN;
+ });
+
it('returns parent identity for singleton identities', () => {
const element = scl.querySelector('Server')!;
expect(identity(element)).to.equal(identity(element.parentElement!));
@@ -27,14 +73,14 @@ describe('identity', () => {
KDC: 'IED1>IED1 P1',
LDevice: 'IED1>>CircuitBreaker_CB1',
IEDName:
- 'IED1>>CircuitBreaker_CB1>GCB>IED2 P1 CircuitBreaker_CB1/ CSWI 1',
+ 'IED1>>CircuitBreaker_CB1>GCB>IED2 P1 CircuitBreaker_CB1/ XCBR 1',
FCDA: 'IED1>>CircuitBreaker_CB1>GooseDataSet1>CircuitBreaker_CB1/ XCBR 1.Pos stVal (ST)',
ExtRef:
- 'IED1>>Disconnectors>DC CSWI 1>GOOSE:GCB CBSW/ LLN0 IED2 CBSW/ XSWI 2 Pos stVal@intAddr',
- 'ExtRef:not([iedName])': 'IED1>>Disconnectors>DC CSWI 1>stVal-t[0]',
+ 'IED1>>Disconnectors>DC CSWI 1>GOOSE:GCB CBSW/ LLN0 IED2 CBSW/ XSWI 2 Pos stVal',
+ 'ExtRef[intAddr="stVal-t"]': 'IED1>>Disconnectors>DC CSWI 1>stVal-t[0]',
LN: 'IED1>>CircuitBreaker_CB1> XCBR 1',
ClientLN:
- 'IED2>>CBSW> XSWI 1>ReportCb>IED1 P1 CircuitBreaker_CB1/ XCBR 1',
+ 'IED2>>CBSW> XSWI 1>ReportCb>IED1 P1 CircuitBreaker_CB1/DC XCBR 1',
DAI: 'IED1>>CircuitBreaker_CB1> XCBR 1>Pos>ctlModel',
SDI: 'IED1>>CircuitBreaker_CB1>CB CSWI 2>Pos>pulseConfig',
Val: 'IED1>>CircuitBreaker_CB1> XCBR 1>Pos>ctlModel> 0',
@@ -47,6 +93,7 @@ describe('identity', () => {
ProtNs: '#Dummy.LLN0.Mod.SBOw>8-MMS\tIEC 61850-8-1:2003',
Association: 'IED1>P1>IED3 MU01/ LLN0 ',
LNode: 'IED2 CBSW/ XSWI 3',
+ 'SDI[ix="2"]': 'IED2>>CircuitBreaker_CB1> MHAN 1>HaAmp>har[2]',
};
Object.keys(expectations).forEach(key => {
diff --git a/utils/identity.ts b/utils/identity.ts
index 7fd9c02..4be9bbb 100644
--- a/utils/identity.ts
+++ b/utils/identity.ts
@@ -50,7 +50,7 @@ function lDeviceIdentity(e: Element): string {
return `${identity(e.closest('IED')!)}>>${e.getAttribute('inst')}`;
}
-function iEDNameIdentity(e: Element): string {
+function iEDNameIdentity(e: Element): string | number {
const iedName = e.textContent;
const [apRef, ldInst, prefix, lnClass, lnInst] = [
'apRef',
@@ -59,6 +59,7 @@ function iEDNameIdentity(e: Element): string {
'lnClass',
'lnInst',
].map(name => e.getAttribute(name));
+
return `${identity(e.parentElement)}>${iedName} ${apRef || ''} ${
ldInst || ''
}/${prefix ?? ''} ${lnClass ?? ''} ${lnInst ?? ''}`;
@@ -91,7 +92,7 @@ function extRefIdentity(e: Element): string | number {
const intAddrIndex = Array.from(
e.parentElement.querySelectorAll(`ExtRef[intAddr="${intAddr}"]`)
).indexOf(e);
- if (!iedName) return `${parentIdentity}>${intAddr}[${intAddrIndex}]`;
+ if (intAddr) return `${parentIdentity}>${intAddr}[${intAddrIndex}]`;
const [
ldInst,
prefix,
@@ -120,18 +121,27 @@ function extRefIdentity(e: Element): string | number {
'srcCBName',
].map(name => e.getAttribute(name));
+ const defaultSrcPrefix = '';
+ const finalSrcPrefix = srcPrefix ?? defaultSrcPrefix;
+
+ const defaultSrcLNInst = '';
+ const finalSrcLNInst = srcLNInst ?? defaultSrcLNInst;
+
const cbPath = srcCBName
- ? `${serviceType}:${srcCBName} ${srcLDInst ?? ''}/${srcPrefix ?? ''} ${
- srcLNClass ?? ''
- } ${srcLNInst ?? ''}`
+ ? `${serviceType}:${srcCBName} ${srcLDInst}/${finalSrcPrefix} ${srcLNClass} ${finalSrcLNInst}`
: '';
- const dataPath = `${iedName} ${ldInst}/${prefix ?? ''} ${lnClass} ${
- lnInst ?? ''
- } ${doName} ${daName || ''}`;
- return `${parentIdentity}>${cbPath ? `${cbPath} ` : ''}${dataPath}${
- // eslint-disable-next-line no-useless-concat
- intAddr ? '@' + `${intAddr}` : ''
- }`;
+
+ const defaultPrefix = '';
+ const finalPrefix = prefix ?? defaultPrefix;
+
+ const defaultLnInst = '';
+ const finalLnInst = lnInst ?? defaultLnInst;
+
+ const defaultDaName = '';
+ const finalDaName = daName || defaultDaName;
+
+ const dataPath = `${iedName} ${ldInst}/${finalPrefix} ${lnClass} ${finalLnInst} ${doName} ${finalDaName}`;
+ return `${parentIdentity}>${cbPath ? `${cbPath} ` : ''}${dataPath}`;
}
function lNIdentity(e: Element): string {
@@ -166,7 +176,7 @@ function valIdentity(e: Element): string | number {
const index = Array.from(e.parentElement.children)
.filter(child => child.getAttribute('sGroup') === sGroup)
.findIndex(child => child.isSameNode(e));
- return `${identity(e.parentElement)}>${sGroup ? `${sGroup}.` : ''} ${index}`;
+ return `${identity(e.parentElement)}>${sGroup ? `${sGroup}` : ''} ${index}`;
}
function connectedAPIdentity(e: Element): string {
@@ -185,7 +195,7 @@ function controlBlockIdentity(e: Element): string {
function physConnIdentity(e: Element): string | number {
if (!e.parentElement) return NaN;
- if (!e.parentElement.querySelector('PhysConn[type="RedConn"]')) return NaN;
+
const pcType = e.getAttribute('type');
if (
e.parentElement.children.length > 1 &&
@@ -212,10 +222,12 @@ function enumValIdentity(e: Element): string {
return `${identity(e.parentElement)}>${e.getAttribute('ord')}`;
}
-function protNsIdentity(e: Element): string {
- return `${identity(e.parentElement)}>${e.getAttribute('type') || '8-MMS'}\t${
- e.textContent
- }`;
+function protNsIdentity(e: Element): string | number {
+ if (!e.parentElement) return NaN;
+
+ const type = e.getAttribute('type');
+
+ return `${identity(e.parentElement)}>${type || '8-MMS'}\t${e.textContent}`;
}
function sCLIdentity(): string {
@@ -481,9 +493,6 @@ const tags: Record<
PowerTransformer: {
identity: namingIdentity,
},
- Private: {
- identity: () => NaN,
- },
Process: {
identity: namingIdentity,
},
diff --git a/utils/tags.spec.ts b/utils/tags.spec.ts
index a436b51..a89eb6a 100644
--- a/utils/tags.spec.ts
+++ b/utils/tags.spec.ts
@@ -1,6 +1,14 @@
import { expect } from '@open-wc/testing';
-import { getReference } from './tags.js';
+import { getReference, parentTags } from './tags.js';
+
+describe('parentTag', () => {
+ it('returns empty string with non SCL tag', () =>
+ expect(parentTags('LNodeSpec').length).to.equal(0));
+
+ it('returns empty all possible parents for SCL tag', () =>
+ expect(parentTags('SDI')).to.deep.equal(['DOI', 'SDI']));
+});
describe('getReference', () => {
it('returns null for invalid SCL tag', () => {
diff --git a/utils/tags.ts b/utils/tags.ts
index 8b48c4c..c227c84 100644
--- a/utils/tags.ts
+++ b/utils/tags.ts
@@ -157,7 +157,7 @@ const sCLTags = [
'SecPerSamples',
] as const;
-type SCLTag = (typeof sCLTags)[number];
+export type SCLTag = (typeof sCLTags)[number];
const tBaseNameSequence = ['Text', 'Private'] as const;
const tNamingSequence = [...tBaseNameSequence] as const;
@@ -833,14 +833,14 @@ export function isSCLTag(tag: string): tag is SCLTag {
}
/** @returns parent `tagName` s for SCL (2007B4) element tag */
-/** export function parentTags(tagName: string): string[] {
+export function parentTags(tagName: string): SCLTag[] {
if (!isSCLTag(tagName)) return [];
return tags[tagName].parents;
-} */
+}
/** @returns child `tagName`s for SCL (2007B4) element tag */
-/** export function childTags(tagName: string): string[] {
+/* export function childTags(tagName: string): string[] {
if (!isSCLTag(tagName)) return [];
return tags[tagName].children;
diff --git a/utils/validScl.ts b/utils/validScl.ts
index 3eec09d..5c3831f 100644
--- a/utils/validScl.ts
+++ b/utils/validScl.ts
@@ -16,9 +16,11 @@ export const validScl = `
+
+
@@ -62,7 +64,7 @@ export const validScl = `
0010
-
+
RJ45
@@ -93,8 +95,12 @@ export const validScl = `
+
+ 01-0C-CD-04-00-20
+
01-0C-CD-04-00-20
007
+ 01-0C-CD-04-00-20
4
4002
@@ -132,9 +138,18 @@ export const validScl = `
+
+ textContent
+ IED2
IED2
+ IED3
+ IED4
+ IED1
+ IED2
+ IED3
+ IED5
@@ -163,7 +178,12 @@ export const validScl = `
- 1
+
+ 1
+
+ 1
+ 2
+ 3
@@ -183,9 +203,13 @@ export const validScl = `
-
-
-
+
+
+
+
+
+
+
@@ -213,7 +237,8 @@ export const validScl = `
-
+
+
@@ -269,7 +294,8 @@ export const validScl = `
-
+
+
@@ -280,6 +306,7 @@ export const validScl = `
+
@@ -300,7 +327,7 @@ export const validScl = `
-
+
status-only
@@ -327,8 +354,40 @@ export const validScl = `
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
@@ -469,6 +528,17 @@ export const validScl = `
+
+
+
+
+
+
+
+
+
+
+
@@ -623,6 +693,7 @@ export const validScl = `
+ IEC 61850-8-1:2003
@@ -631,6 +702,7 @@ export const validScl = `
+ IEC 61850-8-2:2021