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;