diff --git a/describe/ControlWithIEDName.spec.ts b/describe/ControlWithIEDName.spec.ts new file mode 100644 index 00000000..5cda0597 --- /dev/null +++ b/describe/ControlWithIEDName.spec.ts @@ -0,0 +1,80 @@ +import { expect } from "chai"; + +import { describeControlWithIEDName } from "./ControlWithIEDName.js"; + +const scl = new DOMParser().parseFromString( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IED3 + IED1 + IED2 + IED1 + + + IED1 + IED2 + IED3 + IED1 + + + + + + + + + + `, + "application/xml" +); + +const baseGSEControl = scl.querySelector(`*[datSet="baseDataSet"]`)!; +const equalGSEControl = scl.querySelector('*[datSet="equalDataSet"]')!; +const diffGSEControl = scl.querySelector('*[datSet="diffDataSet"]')!; +const invalidDataSet = scl.querySelector('*[datSet="invalidDataSet"]')!; +const invalidReference = scl.querySelector('*[datSet="invalidReference"]')!; + +describe("Description for SCL schema type tControlWithIEDName", () => { + it("returns undefined when referenced DataSet is undefined", () => + expect(describeControlWithIEDName(invalidDataSet)).to.be.undefined); + + it("returns undefined with missing referenced DataSet", () => + expect(describeControlWithIEDName(invalidReference)).to.be.undefined); + + it("returns same description with semantically equal ControlWithIEDName's", () => + expect(JSON.stringify(describeControlWithIEDName(baseGSEControl))).to.equal( + JSON.stringify(describeControlWithIEDName(equalGSEControl)) + )); + + it("returns different description with unequal ControlWithIEDName elements", () => + expect( + JSON.stringify(describeControlWithIEDName(baseGSEControl)) + ).to.not.equal(JSON.stringify(describeControlWithIEDName(diffGSEControl)))); +}); diff --git a/describe/ControlWithIEDName.ts b/describe/ControlWithIEDName.ts new file mode 100644 index 00000000..b5d4368d --- /dev/null +++ b/describe/ControlWithIEDName.ts @@ -0,0 +1,80 @@ +import { ControlDescription, describeControl } from "./Control.js"; + +function compareIEDNameDescription(a: IEDName, b: IEDName): number { + const stringifiedA = JSON.stringify(a); + const stringifiedB = JSON.stringify(b); + + if (stringifiedA < stringifiedB) return -1; + if (stringifiedA > stringifiedB) return 1; + return 0; +} + +type IEDName = { + /** IEDName attribute apRef*/ + apRef?: string; + /** IEDName attribute ldInst*/ + ldInst?: string; + /** IEDName attribute prefix*/ + prefix?: string; + /** IEDName attribute lnClass*/ + lnClass?: string; + /** IEDName attribute lnInst*/ + lnInst?: string; + /** IEDName child text content */ + val?: string; +}; + +function describeIEDName(element: Element): IEDName { + const iedName: IEDName = {}; + + const [apRef, ldInst, prefix, lnClass, lnInst] = [ + "apRef", + "ldInst", + "prefix", + "lnClass", + "lnInst", + ].map((attr) => element.getAttribute(attr)); + + const val = element.textContent; + + if (apRef) iedName.apRef = apRef; + if (ldInst) iedName.ldInst = ldInst; + if (prefix) iedName.prefix = prefix; + if (lnClass) iedName.lnClass = lnClass; + if (lnInst) iedName.lnInst = lnInst; + if (val) iedName.val = val; + + return iedName; +} + +export interface ControlWithIEDNameDescription extends ControlDescription { + /** ControlWithIEDName children IEDName */ + iedNames: IEDName[]; + /** ControlWithIEDName attribute confRev defaulted to 0 */ + confRev?: number; +} + +export function describeControlWithIEDName( + element: Element +): ControlWithIEDNameDescription | undefined { + const controlDescription = describeControl(element); + if (!controlDescription) return; + + const controlWithIEDNameDescription: ControlWithIEDNameDescription = { + ...controlDescription, + iedNames: [], + }; + + controlWithIEDNameDescription.iedNames.push( + ...Array.from(element.children) + .filter((child) => child.tagName === "IEDName") + .map((iedName) => describeIEDName(iedName)) + .sort(compareIEDNameDescription) + ); + + const confRev = element.getAttribute("confRev"); + if (confRev && !isNaN(parseInt(confRev, 10))) + controlWithIEDNameDescription.confRev = parseInt(confRev, 10); + + return controlWithIEDNameDescription; +} diff --git a/describe/GSEControl.spec.ts b/describe/GSEControl.spec.ts new file mode 100644 index 00000000..980eb8c0 --- /dev/null +++ b/describe/GSEControl.spec.ts @@ -0,0 +1,82 @@ +import { expect } from "chai"; + +import { describeGSEControl } from "./GSEControl.js"; + +const scl = new DOMParser().parseFromString( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IED3 + IED1 + IED2 + IED1 + R-GOOSE + + + IED1 + IED2 + IED3 + IED1 + R-GOOSE + + + + + + + + + + `, + "application/xml" +); + +const baseGSEControl = scl.querySelector(`*[datSet="baseDataSet"]`)!; +const equalGSEControl = scl.querySelector('*[datSet="equalDataSet"]')!; +const diffGSEControl = scl.querySelector('*[datSet="diffDataSet"]')!; +const invalidDataSet = scl.querySelector('*[datSet="invalidDataSet"]')!; +const invalidReference = scl.querySelector('*[datSet="invalidReference"]')!; + +describe("Description for SCL schema type tControlWithIEDName", () => { + it("returns undefined when referenced DataSet is undefined", () => + expect(describeGSEControl(invalidDataSet)).to.be.undefined); + + it("returns undefined with missing referenced DataSet", () => + expect(describeGSEControl(invalidReference)).to.be.undefined); + + it("returns same description with semantically equal GSEControl's", () => + expect(JSON.stringify(describeGSEControl(baseGSEControl))).to.equal( + JSON.stringify(describeGSEControl(equalGSEControl)) + )); + + it("returns different description with unequal GSEControl elements", () => + expect(JSON.stringify(describeGSEControl(baseGSEControl))).to.not.equal( + JSON.stringify(describeGSEControl(diffGSEControl)) + )); +}); diff --git a/describe/GSEControl.ts b/describe/GSEControl.ts new file mode 100644 index 00000000..957dee31 --- /dev/null +++ b/describe/GSEControl.ts @@ -0,0 +1,44 @@ +import { + ControlWithIEDNameDescription, + describeControlWithIEDName, +} from "./ControlWithIEDName.js"; + +export interface GSEControlDescription extends ControlWithIEDNameDescription { + /** GSEControl attribute type defaulted to "GOOSE" */ + type: "GOOSE" | "GSSE"; + /** GSEControl attribute appId */ + appID: string; + /** GSEControl attribute fixedOffs defaulted to false */ + fixedOffs: boolean; + /** GSEControl attribute securityEnable defaulted to "None" */ + securityEnable: "None" | "Signature" | "SignatureAndEncryption"; + /**GSEControl child Protocol*/ + protocol?: { mustUnderstand: true; val: "R-GOOSE" }; +} + +export function describeGSEControl( + element: Element +): GSEControlDescription | undefined { + const controlWithTriggerOptDesc = describeControlWithIEDName(element); + if (!controlWithTriggerOptDesc) return; + + const gseControlDescription: GSEControlDescription = { + ...controlWithTriggerOptDesc, + type: element.getAttribute("type") === "GSSE" ? "GSSE" : "GOOSE", + appID: element.getAttribute("appID") ?? "", + fixedOffs: element.getAttribute("fixedOffs") === "true" ? true : false, + securityEnable: element.getAttribute("securityEnable") + ? (element.getAttribute("securityEnable") as + | "Signature" + | "SignatureAndEncryption") + : "None", + }; + + const protocol = Array.from(element.children).find( + (child) => child.tagName === "Protocol" + ); + if (protocol) + gseControlDescription.protocol = { mustUnderstand: true, val: "R-GOOSE" }; + + return gseControlDescription; +}