From 779af85c3a07c07ff229e57c67c45d116a22740c Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Sat, 3 Feb 2024 23:28:52 +0100 Subject: [PATCH] feat: describe IED --- describe.spec.ts | 18 ++--- describe.ts | 5 +- describe/IED.spec.ts | 162 +++++++++++++++++++++++++++++++++++++++++++ describe/IED.ts | 130 ++++++++++++++++++++++++++++++++++ utils.ts | 3 +- 5 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 describe/IED.spec.ts create mode 100644 describe/IED.ts diff --git a/describe.spec.ts b/describe.spec.ts index 6d04ffa7..170e46cd 100644 --- a/describe.spec.ts +++ b/describe.spec.ts @@ -149,9 +149,9 @@ const baseEnumType = testScl.querySelector("#someID")!; const diffEnumType = testScl.querySelector("#someDiffID")!; const equalEnumType = testScl.querySelector("#someOtherID")!; -const baseAP = testScl.querySelector(`IED[name="IED1"]>AccessPoint`)!; -const equalAP = testScl.querySelector(`IED[name="IED2"]>AccessPoint`)!; -const diffAP = testScl.querySelector(`IED[name="IED3"]>AccessPoint`)!; +const baseIED = testScl.querySelector(`IED[name="IED1"]`)!; +const equalIED = testScl.querySelector(`IED[name="IED2"]`)!; +const diffIED = testScl.querySelector(`IED[name="IED3"]`)!; describe("Describe SCL elements function", () => { it("returns undefined with missing describe function", () => @@ -170,13 +170,13 @@ describe("Describe SCL elements function", () => { JSON.stringify(describeSclElement(equalEnumType)), )); - it("returns same description with semantically equal AccessPoint's", () => - expect(JSON.stringify(describeSclElement(baseAP))).to.equal( - JSON.stringify(describeSclElement(equalAP)), + it("returns same description with semantically equal IED's", () => + expect(JSON.stringify(describeSclElement(baseIED))).to.equal( + JSON.stringify(describeSclElement(equalIED)), )); - it("returns different description with unequal AccessPoint elements", () => - expect(JSON.stringify(describeSclElement(baseAP))).to.not.equal( - JSON.stringify(describeSclElement(diffAP)), + it("returns different description with unequal IED elements", () => + expect(JSON.stringify(describeSclElement(baseIED))).to.not.equal( + JSON.stringify(describeSclElement(diffIED)), )); }); diff --git a/describe.ts b/describe.ts index de21ffac..54533ebd 100644 --- a/describe.ts +++ b/describe.ts @@ -1,4 +1,5 @@ import { AccessPoint, AccessPointDescription } from "./describe/AccessPoint.js"; +import { IED, IEDDescription } from "./describe/IED.js"; import { Private, PrivateDescription } from "./describe/Private.js"; import { Text, TextDescription } from "./describe/Text.js"; import { EnumType, EnumTypeDescription } from "./describe/EnumType.js"; @@ -24,7 +25,8 @@ export type Description = | LDeviceDescription | ServerDescription | ServicesDescription - | AccessPointDescription; + | AccessPointDescription + | IEDDescription; const sclElementDescriptors: Partial< Record Description | undefined> > = { @@ -40,6 +42,7 @@ const sclElementDescriptors: Partial< Server, Services, AccessPoint, + IED, }; export function describe(element: Element): Description | undefined { diff --git a/describe/IED.spec.ts b/describe/IED.spec.ts new file mode 100644 index 00000000..99cba91c --- /dev/null +++ b/describe/IED.spec.ts @@ -0,0 +1,162 @@ +import { expect } from "chai"; + +import { IED } from "./IED"; + +const scl = new DOMParser().parseFromString( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + off + + + + + + + + + + + + + + 60.60 + 10.10 + 40.10 + + + + on + test + off + + + `, + "application/xml", +); + +const baseIED = scl.querySelector('IED[name="IED1"]')!; +const equalIED = scl.querySelector('IED[name="IED2"]')!; +const diffIED = scl.querySelector('IED[name="IED3"]')!; +const invalidIED1 = scl.querySelector('IED[name="IED4"]')!; +const invalidIED2 = scl.querySelector('IED[name="IED5"]')!; + +describe("Description for SCL schema type IED", () => { + it("returns undefined with invalid AccessPoint", () => { + expect(IED(invalidIED1)).to.be.undefined; + expect(IED(invalidIED2)).to.be.undefined; + }); + + it("return originalSclVersion attribute defaulting to 2003", () => + expect(IED(baseIED)?.originalSclVersion).to.be.equal(2003)); + + it("return originalSclRevision attribute defaulting to 'A'", () => + expect(IED(baseIED)?.originalSclRevision).to.be.equal("A")); + + it("return originalSclRelease attribute defaulting to 1", () => + expect(IED(baseIED)?.originalSclRelease).to.be.equal(1)); + + it("return type attribute ", () => + expect(IED(diffIED)?.type).to.be.equal("type")); + + it("return manufacturer attribute ", () => + expect(IED(diffIED)?.manufacturer).to.be.equal("manufacturer")); + + it("return configVersion attribute ", () => + expect(IED(diffIED)?.configVersion).to.be.equal("3")); + + it("return owner attribute ", () => + expect(IED(diffIED)?.owner).to.be.equal("owner")); + + it("returns same description with semantically equal IED's", () => + expect(JSON.stringify(IED(baseIED))).to.equal( + JSON.stringify(IED(equalIED)), + )); + + it("returns different description with unequal IED elements", () => + expect(JSON.stringify(IED(baseIED))).to.not.equal( + JSON.stringify(IED(diffIED)), + )); +}); diff --git a/describe/IED.ts b/describe/IED.ts new file mode 100644 index 00000000..2ba1226f --- /dev/null +++ b/describe/IED.ts @@ -0,0 +1,130 @@ +import { sortRecord } from "../utils.js"; +import { AccessPoint, AccessPointDescription } from "./AccessPoint.js"; +import { NamingDescription, describeNaming } from "./Naming.js"; +import { Services, ServicesDescription } from "./Services.js"; + +function compareKDCs(a: KDCDescription, b: KDCDescription): number { + const stringifiedA = JSON.stringify(a); + const stringifiedB = JSON.stringify(b); + + if (stringifiedA < stringifiedB) return -1; + if (stringifiedA > stringifiedB) return 1; + return 0; +} + +interface KDCDescription { + /** IED attribute iedName */ + iedName: string; + /** IED attribute apName */ + apName: string; +} + +export interface IEDDescription extends NamingDescription { + /** IED attribute type */ + type?: string; + /** IED attribute manufacturer */ + manufacturer?: string; + /** IED attribute configVersion */ + configVersion?: string; + /** IED attribute originalSclVersion defaulting 2003*/ + originalSclVersion: number; + /** IED attribute originalSclRevision defaulting "A"*/ + originalSclRevision: string; + /** IED attribute originalSclRelease defaulting 1*/ + originalSclRelease: number; + /** IED attribute engRight defaulting "full" */ + engRight: string; + /** IED attribute owner */ + owner?: string; + /** IED child Services */ + services?: ServicesDescription; + /** IED children AccessPoint */ + accessPoints: Record; + /** IED children KDC */ + kDCs: KDCDescription[]; +} + +function kdcDescription(element: Element): KDCDescription | undefined { + const iedName = element.getAttribute("iedName"); + const apName = element.getAttribute("apName"); + if (!iedName || !apName) return; + + return { iedName, apName }; +} + +function kDCs(parent: Element): KDCDescription[] { + const kdcDescriptions: KDCDescription[] = []; + parent.querySelectorAll(":scope > KDC").forEach((kdc) => { + const kdcDesc = kdcDescription(kdc); + if (kdcDesc) kdcDescriptions.push(kdcDesc); + }); + + return kdcDescriptions.sort(compareKDCs); +} + +function sortedAccessPointDescriptions( + parent: Element, +): Record | undefined { + const accessPoints: Record = {}; + let existUndefinedAPs = false; + Array.from(parent.querySelectorAll(":scope > AccessPoint")).forEach( + (accessPoint) => { + const name = accessPoint.getAttribute("name"); + if (!name) { + existUndefinedAPs = true; + return; + } + + const accessPointDescription = AccessPoint(accessPoint); + if (!accessPointDescription) { + existUndefinedAPs = true; + return; + } + + accessPoints[name] = accessPointDescription; + }, + ); + if (existUndefinedAPs) return; + + return sortRecord(accessPoints) as Record; +} + +export function IED(element: Element): IEDDescription | undefined { + const accessPoints = sortedAccessPointDescriptions(element); + if (!accessPoints) return; + + const iedDescription: IEDDescription = { + ...describeNaming(element), + originalSclVersion: element.getAttribute("originalSclVersion") + ? parseInt(element.getAttribute("originalSclVersion")!, 10) + : 2003, + originalSclRevision: element.getAttribute("originalSclRevision") + ? element.getAttribute("originalSclRevision")! + : "A", + originalSclRelease: element.getAttribute("originalSclRelease") + ? parseInt(element.getAttribute("originalSclRelease")!, 10) + : 1, + engRight: element.getAttribute("engRight") + ? element.getAttribute("engRight")! + : "full", + accessPoints, + kDCs: kDCs(element), + }; + + const type = element.getAttribute("type"); + if (type) iedDescription.type = type; + + const manufacturer = element.getAttribute("manufacturer"); + if (manufacturer) iedDescription.manufacturer = manufacturer; + + const configVersion = element.getAttribute("configVersion"); + if (configVersion) iedDescription.configVersion = configVersion; + + const owner = element.getAttribute("owner"); + if (owner) iedDescription.owner = owner; + + const servicesElement = element.querySelector(":scope > Services"); + if (servicesElement) iedDescription.services = Services(servicesElement); + + return iedDescription; +} diff --git a/utils.ts b/utils.ts index 25dae21d..a367a78a 100644 --- a/utils.ts +++ b/utils.ts @@ -1,4 +1,4 @@ -import { Certificate } from "./describe/AccessPoint.js"; +import { AccessPointDescription, Certificate } from "./describe/AccessPoint.js"; import { DADescription } from "./describe/DADescription.js"; import { DODescription } from "./describe/DODescription.js"; import { GSEControlDescription } from "./describe/GSEControl.js"; @@ -11,6 +11,7 @@ import { SDODescription } from "./describe/SDODescription.js"; import { SampledValueControlDescription } from "./describe/SampledValueControl.js"; type SortedObjects = + | AccessPointDescription | Certificate | DADescription | GSEControlDescription