diff --git a/describe.spec.ts b/describe.spec.ts
index 0f48a31f..6b0e7b9c 100644
--- a/describe.spec.ts
+++ b/describe.spec.ts
@@ -11,15 +11,31 @@ const testScl = new DOMParser().parseFromString(
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -102,6 +118,10 @@ const baseLN = testScl.querySelector(`LN[lnType="baseXCBR"]`)!;
const equalLN = testScl.querySelector(`LN[lnType="equalXCBR"]`)!;
const diffLN = testScl.querySelector(`LN[lnType="diffXCBR"]`)!;
+const baseLN0 = testScl.querySelector(`LDevice[inst="ldInst1"]>LN0`)!;
+const equalLN0 = testScl.querySelector(`LDevice[inst="ldInst2"]>LN0`)!;
+const diffLN0 = testScl.querySelector(`LDevice[inst="ldInst3"]>LN0`)!;
+
describe("Describe SCL elements function", () => {
it("returns undefined with missing describe function", () =>
expect(describeSclElement(oneNonSCLElement)).to.be.undefined);
@@ -119,6 +139,16 @@ describe("Describe SCL elements function", () => {
JSON.stringify(describeSclElement(equalEnumType)),
));
+ it("returns same description with semantically equal LN0's", () =>
+ expect(JSON.stringify(describeSclElement(baseLN0))).to.equal(
+ JSON.stringify(describeSclElement(equalLN0)),
+ ));
+
+ it("returns different description with unequal LN0 elements", () =>
+ expect(JSON.stringify(describeSclElement(baseLN0))).to.not.equal(
+ JSON.stringify(describeSclElement(diffLN0)),
+ ));
+
it("returns same description with semantically equal LN's", () =>
expect(JSON.stringify(describeSclElement(baseLN))).to.equal(
JSON.stringify(describeSclElement(equalLN)),
diff --git a/describe.ts b/describe.ts
index e9b98bcf..f65f066d 100644
--- a/describe.ts
+++ b/describe.ts
@@ -5,6 +5,7 @@ import { DAType, DATypeDescription } from "./describe/DAType.js";
import { DOType, DOTypeDescription } from "./describe/DOType.js";
import { LNodeType, LNodeTypeDescription } from "./describe/LNodeType.js";
import { LN, LNDescription } from "./describe/LN.js";
+import { LN0, LN0Description } from "./describe/LN0.js";
export type Description =
| PrivateDescription
@@ -14,7 +15,8 @@ export type Description =
| DATypeDescription
| DOTypeDescription
| LNodeTypeDescription
- | LNDescription;
+ | LNDescription
+ | LN0Description;
const sclElementDescriptors: Partial<
Record Description | undefined>
> = {
@@ -25,6 +27,7 @@ const sclElementDescriptors: Partial<
DOType,
LNodeType,
LN,
+ LN0,
};
export function describe(element: Element): Description | undefined {
diff --git a/describe/ControlWithIEDName.spec.ts b/describe/ControlWithIEDName.spec.ts
index 5cda0597..3a6f0146 100644
--- a/describe/ControlWithIEDName.spec.ts
+++ b/describe/ControlWithIEDName.spec.ts
@@ -52,7 +52,7 @@ const scl = new DOMParser().parseFromString(
`,
- "application/xml"
+ "application/xml",
);
const baseGSEControl = scl.querySelector(`*[datSet="baseDataSet"]`)!;
@@ -70,11 +70,11 @@ describe("Description for SCL schema type tControlWithIEDName", () => {
it("returns same description with semantically equal ControlWithIEDName's", () =>
expect(JSON.stringify(describeControlWithIEDName(baseGSEControl))).to.equal(
- JSON.stringify(describeControlWithIEDName(equalGSEControl))
+ JSON.stringify(describeControlWithIEDName(equalGSEControl)),
));
it("returns different description with unequal ControlWithIEDName elements", () =>
expect(
- JSON.stringify(describeControlWithIEDName(baseGSEControl))
+ JSON.stringify(describeControlWithIEDName(baseGSEControl)),
).to.not.equal(JSON.stringify(describeControlWithIEDName(diffGSEControl))));
});
diff --git a/describe/ControlWithIEDName.ts b/describe/ControlWithIEDName.ts
index b5d4368d..ed3c5005 100644
--- a/describe/ControlWithIEDName.ts
+++ b/describe/ControlWithIEDName.ts
@@ -9,7 +9,7 @@ function compareIEDNameDescription(a: IEDName, b: IEDName): number {
return 0;
}
-type IEDName = {
+interface IEDName {
/** IEDName attribute apRef*/
apRef?: string;
/** IEDName attribute ldInst*/
@@ -22,7 +22,7 @@ type IEDName = {
lnInst?: string;
/** IEDName child text content */
val?: string;
-};
+}
function describeIEDName(element: Element): IEDName {
const iedName: IEDName = {};
@@ -55,7 +55,7 @@ export interface ControlWithIEDNameDescription extends ControlDescription {
}
export function describeControlWithIEDName(
- element: Element
+ element: Element,
): ControlWithIEDNameDescription | undefined {
const controlDescription = describeControl(element);
if (!controlDescription) return;
@@ -69,7 +69,7 @@ export function describeControlWithIEDName(
...Array.from(element.children)
.filter((child) => child.tagName === "IEDName")
.map((iedName) => describeIEDName(iedName))
- .sort(compareIEDNameDescription)
+ .sort(compareIEDNameDescription),
);
const confRev = element.getAttribute("confRev");
diff --git a/describe/GSEControl.spec.ts b/describe/GSEControl.spec.ts
index 980eb8c0..f3181cb3 100644
--- a/describe/GSEControl.spec.ts
+++ b/describe/GSEControl.spec.ts
@@ -54,7 +54,7 @@ const scl = new DOMParser().parseFromString(
`,
- "application/xml"
+ "application/xml",
);
const baseGSEControl = scl.querySelector(`*[datSet="baseDataSet"]`)!;
@@ -72,11 +72,11 @@ describe("Description for SCL schema type tControlWithIEDName", () => {
it("returns same description with semantically equal GSEControl's", () =>
expect(JSON.stringify(describeGSEControl(baseGSEControl))).to.equal(
- JSON.stringify(describeGSEControl(equalGSEControl))
+ JSON.stringify(describeGSEControl(equalGSEControl)),
));
it("returns different description with unequal GSEControl elements", () =>
expect(JSON.stringify(describeGSEControl(baseGSEControl))).to.not.equal(
- JSON.stringify(describeGSEControl(diffGSEControl))
+ JSON.stringify(describeGSEControl(diffGSEControl)),
));
});
diff --git a/describe/GSEControl.ts b/describe/GSEControl.ts
index 957dee31..3f1f8cb6 100644
--- a/describe/GSEControl.ts
+++ b/describe/GSEControl.ts
@@ -17,7 +17,7 @@ export interface GSEControlDescription extends ControlWithIEDNameDescription {
}
export function describeGSEControl(
- element: Element
+ element: Element,
): GSEControlDescription | undefined {
const controlWithTriggerOptDesc = describeControlWithIEDName(element);
if (!controlWithTriggerOptDesc) return;
@@ -35,7 +35,7 @@ export function describeGSEControl(
};
const protocol = Array.from(element.children).find(
- (child) => child.tagName === "Protocol"
+ (child) => child.tagName === "Protocol",
);
if (protocol)
gseControlDescription.protocol = { mustUnderstand: true, val: "R-GOOSE" };
diff --git a/describe/LN0.spec.ts b/describe/LN0.spec.ts
new file mode 100644
index 00000000..3776837e
--- /dev/null
+++ b/describe/LN0.spec.ts
@@ -0,0 +1,184 @@
+import { expect } from "chai";
+import { LN } from "./LN.js";
+import { LNodeType, isLNodeTypeDescription } from "./LNodeType.js";
+import { LN0 } from "./LN0.js";
+
+const scl = new DOMParser().parseFromString(
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ on
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ off
+
+
+
+
+ on
+
+
+
+
+ on
+
+
+
+
+ on
+ test
+ off
+
+
+ on
+ test
+
+
+ `,
+ "application/xml",
+);
+
+const missingLnType = scl.querySelector('LDevice[inst="lDevice5"] > LN0')!;
+const invalidLnType = scl.querySelector('LDevice[inst="lDevice6"] > LN0')!;
+const invalidLnTypeDescription = scl.querySelector(
+ 'LDevice[inst="lDevice7"] > LN0',
+)!;
+
+const baseLLN0 = scl.querySelector(`LDevice[inst="lDevice1"] > LN0`)!;
+const equalLLN0 = scl.querySelector('LDevice[inst="lDevice2"] > LN0')!;
+const diffLLN0 = scl.querySelector('LDevice[inst="lDevice3"] > LN0')!;
+const diffEnumType = scl.querySelector('LDevice[inst="lDevice4"] > LN0')!;
+
+describe("Description for SCL schema type LN0", () => {
+ it("returns undefined with missing lnType attribute", () =>
+ expect(LN0(missingLnType)).to.be.undefined);
+
+ it("returns undefined with invalid lnType attribute", () =>
+ expect(LN0(invalidLnType)).to.be.undefined);
+
+ it("returns undefined with invalid LNodeType description", () =>
+ expect(LN0(invalidLnTypeDescription)).to.be.undefined);
+
+ it("return logical node structure in lnType key", () =>
+ expect(LN0(baseLLN0)?.lnType).to.satisfy(isLNodeTypeDescription));
+
+ it("returns same description with semantically equal LN0's", () => {
+ expect(JSON.stringify(LN0(baseLLN0))).to.equal(
+ JSON.stringify(LN0(equalLLN0)),
+ );
+ });
+
+ it("returns different description with unequal LN0 elements", () => {
+ expect(JSON.stringify(LN0(baseLLN0))).to.not.equal(
+ JSON.stringify(LN0(diffLLN0)),
+ );
+ expect(JSON.stringify(LN0(baseLLN0))).to.not.equal(
+ JSON.stringify(LN0(diffEnumType)),
+ );
+ });
+});
diff --git a/describe/LN0.ts b/describe/LN0.ts
new file mode 100644
index 00000000..4b59435d
--- /dev/null
+++ b/describe/LN0.ts
@@ -0,0 +1,76 @@
+import { sortRecord } from "../utils.js";
+
+import { GSEControlDescription, describeGSEControl } from "./GSEControl.js";
+import { LN, LNDescription } from "./LN.js";
+import {
+ SampledValueControlDescription,
+ describeSampledValueControl,
+} from "./SampledValueControl.js";
+import {
+ SettingControlDescription,
+ describeSettingControl,
+} from "./SettingControl.js";
+
+export interface LN0Description extends LNDescription {
+ gseControls: Record;
+ smvControls: Record;
+ settingControl?: SettingControlDescription;
+}
+
+function gseControls(element: Element): Record {
+ const unsortedGSEControls: Record = {};
+
+ Array.from(element.children)
+ .filter((child) => child.tagName === "GSEControl")
+ .forEach((gseControl) => {
+ const name = gseControl.getAttribute("name");
+ const gseControlDescription = describeGSEControl(gseControl);
+ if (name && !unsortedGSEControls[name] && gseControlDescription)
+ unsortedGSEControls[name] = gseControlDescription;
+ });
+
+ return sortRecord(unsortedGSEControls) as Record<
+ string,
+ GSEControlDescription
+ >;
+}
+
+function smvControls(
+ element: Element,
+): Record {
+ const unsortedSampledValueControls: Record<
+ string,
+ SampledValueControlDescription
+ > = {};
+
+ Array.from(element.children)
+ .filter((child) => child.tagName === "SampledValueControl")
+ .forEach((smvControl) => {
+ const name = smvControl.getAttribute("name");
+ const smvControlDescription = describeSampledValueControl(smvControl);
+ if (name && !unsortedSampledValueControls[name] && smvControlDescription)
+ unsortedSampledValueControls[name] = smvControlDescription;
+ });
+
+ return sortRecord(unsortedSampledValueControls) as Record<
+ string,
+ SampledValueControlDescription
+ >;
+}
+
+export function LN0(element: Element): LN0Description | undefined {
+ const lnDescription = LN(element);
+ if (!lnDescription) return;
+
+ const ln0Description: LN0Description = {
+ ...lnDescription,
+ gseControls: gseControls(element),
+ smvControls: smvControls(element),
+ };
+
+ const settingControl = element.querySelector(":scope > SettingControl");
+ if (settingControl && describeSettingControl(settingControl))
+ ln0Description.settingControl = describeSettingControl(settingControl);
+
+ return ln0Description;
+}
diff --git a/describe/SampledValueControl.spec.ts b/describe/SampledValueControl.spec.ts
index 18dd47ac..e9340f97 100644
--- a/describe/SampledValueControl.spec.ts
+++ b/describe/SampledValueControl.spec.ts
@@ -65,7 +65,7 @@ const scl = new DOMParser().parseFromString(
`,
- "application/xml"
+ "application/xml",
);
const baseSampledValueControl = scl.querySelector(`*[datSet="baseDataSet"]`)!;
@@ -74,13 +74,13 @@ const diffSampledValueControl = scl.querySelector('*[datSet="diffDataSet"]')!;
const invalidDataSet = scl.querySelector('*[datSet="invalidDataSet"]')!;
const invalidReference = scl.querySelector('*[datSet="invalidReference"]')!;
const missingSmpRate = scl.querySelector(
- 'SampledValueControl[name="missingSmpRate"]'
+ 'SampledValueControl[name="missingSmpRate"]',
)!;
const missingNofASDU = scl.querySelector(
- 'SampledValueControl[name="missingNofASDU"]'
+ 'SampledValueControl[name="missingNofASDU"]',
)!;
const missingSmpOpts = scl.querySelector(
- 'SampledValueControl[name="missingSmpOpts"]'
+ 'SampledValueControl[name="missingSmpOpts"]',
)!;
describe("Description for SCL schema type SampledValueControl", () => {
@@ -101,16 +101,16 @@ describe("Description for SCL schema type SampledValueControl", () => {
it("returns same description with semantically equal SampledValueControl's", () =>
expect(
- JSON.stringify(describeSampledValueControl(baseSampledValueControl))
+ JSON.stringify(describeSampledValueControl(baseSampledValueControl)),
).to.equal(
- JSON.stringify(describeSampledValueControl(equalSampledValueControl))
+ JSON.stringify(describeSampledValueControl(equalSampledValueControl)),
));
it("returns different description with unequal SampledValueControl elements", () => {
expect(
- JSON.stringify(describeSampledValueControl(baseSampledValueControl))
+ JSON.stringify(describeSampledValueControl(baseSampledValueControl)),
).to.not.equal(
- JSON.stringify(describeSampledValueControl(diffSampledValueControl))
+ JSON.stringify(describeSampledValueControl(diffSampledValueControl)),
);
});
});
diff --git a/describe/SampledValueControl.ts b/describe/SampledValueControl.ts
index 33075ad3..af600e54 100644
--- a/describe/SampledValueControl.ts
+++ b/describe/SampledValueControl.ts
@@ -3,7 +3,7 @@ import {
describeControlWithIEDName,
} from "./ControlWithIEDName.js";
-type SmvOpts = {
+interface SmvOpts {
/** SmvOpts attribute refreshTime defaulted to false */
refreshTime: boolean;
/** SmvOpts attribute sampleSynchronized defaulted to true */
@@ -18,7 +18,7 @@ type SmvOpts = {
timestamp: boolean;
/** SmvOpts attribute synchSourceId defaulted to false */
synchSourceId: boolean;
-};
+}
function smvOpts(element: Element): SmvOpts | undefined {
const smvOpts = element.querySelector(":scope > SmvOpts");
@@ -60,7 +60,7 @@ export interface SampledValueControlDescription
}
export function describeSampledValueControl(
- element: Element
+ element: Element,
): SampledValueControlDescription | undefined {
const controlWithTriggerOptDesc = describeControlWithIEDName(element);
if (!controlWithTriggerOptDesc) return;
@@ -95,7 +95,7 @@ export function describeSampledValueControl(
};
const protocol = Array.from(element.children).find(
- (child) => child.tagName === "Protocol"
+ (child) => child.tagName === "Protocol",
);
if (protocol)
gseControlDescription.protocol = { mustUnderstand: true, val: "R-SV" };
diff --git a/describe/SettingControl.spec.ts b/describe/SettingControl.spec.ts
new file mode 100644
index 00000000..35dac96d
--- /dev/null
+++ b/describe/SettingControl.spec.ts
@@ -0,0 +1,50 @@
+import { expect } from "chai";
+
+import { describeSettingControl } from "./SettingControl.js";
+
+const scl = new DOMParser().parseFromString(
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ "application/xml",
+);
+
+const baseSettingControl = scl.querySelector(
+ 'LN0[desc="base"] > SettingControl',
+)!;
+const equalSettingControl = scl.querySelector(
+ 'LN0[desc="equal"] > SettingControl',
+)!;
+const diffSettingControl = scl.querySelector(
+ 'LN0[desc="diff"] > SettingControl',
+)!;
+const invalidSettingControl = scl.querySelector(
+ 'LN0[desc="invalid"] > SettingControl',
+)!;
+
+describe("Describes the SCL element SettingControl", () => {
+ it("returns undefined with missing numOfSGs ", () =>
+ expect(describeSettingControl(invalidSettingControl)).to.be.undefined);
+
+ it("returns equal description with semantically equal SettingControl element", () =>
+ expect(JSON.stringify(describeSettingControl(baseSettingControl))).to.equal(
+ JSON.stringify(describeSettingControl(equalSettingControl)),
+ ));
+
+ it("returns different description with semantically different SettingControl element", () => {
+ expect(
+ JSON.stringify(describeSettingControl(baseSettingControl)),
+ ).to.not.equal(JSON.stringify(describeSettingControl(diffSettingControl)));
+ });
+});
diff --git a/describe/SettingControl.ts b/describe/SettingControl.ts
new file mode 100644
index 00000000..25def928
--- /dev/null
+++ b/describe/SettingControl.ts
@@ -0,0 +1,29 @@
+import { NamingDescription, describeNaming } from "./Naming.js";
+
+export interface SettingControlDescription extends NamingDescription {
+ numOfSGs: number;
+ actSG?: number;
+ resvTms?: number;
+}
+
+export function describeSettingControl(
+ element: Element,
+): SettingControlDescription | undefined {
+ const numOfSGs = element.getAttribute("numOfSGs");
+ if (!numOfSGs || isNaN(parseInt(numOfSGs, 10))) return;
+
+ const settingGroupDescription: SettingControlDescription = {
+ ...describeNaming(element),
+ numOfSGs: parseInt(numOfSGs, 10),
+ };
+
+ const actSG = element.getAttribute("actSG");
+ if (actSG && !isNaN(parseInt(actSG, 10)))
+ settingGroupDescription.actSG = parseInt(actSG, 10);
+
+ const resvTms = element.getAttribute("resvTms");
+ if (resvTms && !isNaN(parseInt(resvTms, 10)))
+ settingGroupDescription.resvTms = parseInt(resvTms, 10);
+
+ return settingGroupDescription;
+}
diff --git a/utils.ts b/utils.ts
index cfbd830e..2ab18c85 100644
--- a/utils.ts
+++ b/utils.ts
@@ -1,14 +1,18 @@
import { DADescription } from "./describe/DADescription.js";
import { DODescription } from "./describe/DODescription.js";
+import { GSEControlDescription } from "./describe/GSEControl.js";
import { LogControlDescription } from "./describe/LogControl.js";
import { NamingDescription } from "./describe/Naming.js";
import { ReportControlDescription } from "./describe/ReportControl.js";
import { SDODescription } from "./describe/SDODescription.js";
+import { SampledValueControlDescription } from "./describe/SampledValueControl.js";
type SortedObjects =
| DADescription
+ | GSEControlDescription
| LogControlDescription
| NamingDescription
+ | SampledValueControlDescription
| SDODescription
| ReportControlDescription
| DODescription;