diff --git a/describe.spec.ts b/describe.spec.ts index 8222f419..88d46008 100644 --- a/describe.spec.ts +++ b/describe.spec.ts @@ -8,9 +8,10 @@ const testScl = new DOMParser().parseFromString( xmlns:sxy="http://www.iec.ch/61850/2003/SCLcoordinates" xmlns:ens="http://somevalidURI" > - + + @@ -26,6 +27,40 @@ const testScl = new DOMParser().parseFromString( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -114,9 +149,15 @@ const baseEnumType = testScl.querySelector("#someID")!; const diffEnumType = testScl.querySelector("#someDiffID")!; const equalEnumType = testScl.querySelector("#someOtherID")!; -const baseLDevice = testScl.querySelector(`LDevice[inst="ldInst1"]`)!; -const equalLDevice = testScl.querySelector(`LDevice[inst="ldInst2"]`)!; -const diffLDevice = testScl.querySelector(`LDevice[inst="ldInst3"]`)!; +const baseServer = testScl.querySelector( + `IED[name="IED1"] LDevice[inst="ldInst1"]`, +)!; +const equalServer = testScl.querySelector( + `IED[name="IED2"] LDevice[inst="ldInst2"]`, +)!; +const diffServer = testScl.querySelector( + `IED[name="IED2"] LDevice[inst="ldInst3"]`, +)!; describe("Describe SCL elements function", () => { it("returns undefined with missing describe function", () => @@ -136,12 +177,12 @@ describe("Describe SCL elements function", () => { )); it("returns same description with semantically equal LDevice's", () => - expect(JSON.stringify(describeSclElement(baseLDevice))).to.equal( - JSON.stringify(describeSclElement(equalLDevice)), + expect(JSON.stringify(describeSclElement(baseServer))).to.equal( + JSON.stringify(describeSclElement(equalServer)), )); it("returns different description with unequal LDevice elements", () => - expect(JSON.stringify(describeSclElement(baseLDevice))).to.not.equal( - JSON.stringify(describeSclElement(diffLDevice)), + expect(JSON.stringify(describeSclElement(baseServer))).to.not.equal( + JSON.stringify(describeSclElement(diffServer)), )); }); diff --git a/describe.ts b/describe.ts index c5da5b88..07e1ae4b 100644 --- a/describe.ts +++ b/describe.ts @@ -7,6 +7,7 @@ import { LDevice, LDeviceDescription } from "./describe/LDevice.js"; import { LNodeType, LNodeTypeDescription } from "./describe/LNodeType.js"; import { LN, LNDescription } from "./describe/LN.js"; import { LN0, LN0Description } from "./describe/LN0.js"; +import { Server, ServerDescription } from "./describe/Server.js"; export type Description = | PrivateDescription @@ -18,7 +19,8 @@ export type Description = | LNodeTypeDescription | LNDescription | LN0Description - | LDeviceDescription; + | LDeviceDescription + | ServerDescription; const sclElementDescriptors: Partial< Record Description | undefined> > = { @@ -31,6 +33,7 @@ const sclElementDescriptors: Partial< LN, LN0, LDevice, + Server, }; export function describe(element: Element): Description | undefined { diff --git a/describe/Server.spec.ts b/describe/Server.spec.ts new file mode 100644 index 00000000..f358624c --- /dev/null +++ b/describe/Server.spec.ts @@ -0,0 +1,131 @@ +import { expect } from "chai"; + +import { Server } from "./Server.js"; + +const scl = new DOMParser().parseFromString( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + off + + + + + + + + + + + + + + 60.60 + 10.10 + 40.10 + + + + on + test + off + + + `, + "application/xml", +); + +const baseServer = scl.querySelector('IED[name="IED1"] Server')!; +const equalServer = scl.querySelector('IED[name="IED2"] Server')!; +const diffServer = scl.querySelector('IED[name="IED3"] Server')!; +const invalidServer = scl.querySelector('IED[name="IED4"] Server')!; + +describe("Description for SCL schema type LDevice", () => { + it("returns undefined with missing lnType attribute", () => + expect(Server(invalidServer)).to.be.undefined); + + it("default timeout attribute if present", () => + expect(Server(equalServer)?.timeout).to.equal(30)); + + it("default timeout attribute if present", () => + expect(Server(diffServer)?.timeout).to.equal(13)); + + it("returns same description with semantically equal LDevice's", () => + expect(JSON.stringify(Server(baseServer))).to.equal( + JSON.stringify(Server(equalServer)), + )); + + it("returns different description with unequal LDevice elements", () => + expect(JSON.stringify(Server(baseServer))).to.not.equal( + JSON.stringify(Server(diffServer)), + )); +}); diff --git a/describe/Server.ts b/describe/Server.ts new file mode 100644 index 00000000..e10ef1e3 --- /dev/null +++ b/describe/Server.ts @@ -0,0 +1,132 @@ +import { sortRecord } from "../utils.js"; +import { LDevice, LDeviceDescription } from "./LDevice.js"; +import { NamingDescription, describeNaming } from "./Naming.js"; + +function compareAssociations(a: Association, b: Association): number { + const stringifiedA = JSON.stringify(a); + const stringifiedB = JSON.stringify(b); + + if (stringifiedA < stringifiedB) return -1; + if (stringifiedA > stringifiedB) return 1; + return 0; +} + +interface Authentication { + /** Authentication attribute none defaulted to true */ + none: boolean; + /** Authentication attribute password defaulted to false */ + password: boolean; + /** Authentication attribute weak defaulted to false */ + weak: boolean; + /** Authentication attribute strong defaulted to false */ + strong: boolean; + /** Authentication attribute certificate defaulted to false */ + certificate: boolean; +} + +interface Association { + desc?: string; + iedName: string; + ldInst: string; + /** Association attribute prefix defaulted to empty string */ + prefix: string; + /** Association attribute lnClass */ + lnClass: string; + /** Association attribute lnInst */ + lnInst: string; + /** Association attribute kind */ + kind: "pre-established" | "predefined"; + /** Association attribute associationId */ + associationId?: string; +} + +export interface ServerDescription extends NamingDescription { + /** Server attribute timeout defaulted to 30 */ + timeout: number; + /** Server child Authentication */ + authentication: Authentication; + /** Server children LDevice */ + lDevices: Record; + /** Server children Association */ + associations: Association[]; +} + +function associations(element: Element): Association[] { + const associations: Association[] = []; + Array.from(element.children) + .filter((child) => child.tagName === "Association") + .forEach((assoc) => { + const kind = assoc.getAttribute("kind"); + const associationId = assoc.getAttribute("associationId"); + const iedName = assoc.getAttribute("iedName"); + const ldInst = assoc.getAttribute("ldInst"); + const desc = assoc.getAttribute("desc") ?? ""; + const prefix = assoc.getAttribute("prefix") ?? ""; + const lnClass = assoc.getAttribute("lnClass"); + const lnInst = assoc.getAttribute("lnInst"); + + if ( + !kind || + !["pre-established", "predefined"].includes(kind) || + !iedName || + !ldInst || + !lnClass || + lnInst === null + ) + return; + + const association: Association = { + kind: kind as "pre-established" | "predefined", + desc, + iedName, + ldInst, + prefix, + lnClass, + lnInst, + }; + if (associationId) association.associationId = associationId; + + associations.push(association); + }); + + return associations.sort(compareAssociations); +} + +function authentication(element: Element): Authentication { + return { + none: element.getAttribute("none") === "false" ? false : true, + password: element.getAttribute("password") === "true" ? true : false, + weak: element.getAttribute("weak") === "true" ? true : false, + strong: element.getAttribute("strong") === "true" ? true : false, + certificate: element.getAttribute("certificate") === "true" ? true : false, + }; +} + +function lDevices(element: Element): Record { + const unsortedLDevices: Record = {}; + Array.from(element.children) + .filter((child) => child.tagName === "LDevice") + .forEach((lDevice) => { + const inst = lDevice.getAttribute("inst"); + const lDeviceDescription = LDevice(lDevice); + if (inst && !unsortedLDevices[inst] && lDeviceDescription) + unsortedLDevices[inst] = lDeviceDescription; + }); + + return sortRecord(unsortedLDevices) as Record; +} + +export function Server(element: Element): ServerDescription | undefined { + const auth = element.querySelector(":scope > Authentication"); + if (!auth) return; + + const serverDescription: ServerDescription = { + ...describeNaming(element), + timeout: parseInt(element.getAttribute("timeout") ?? "30", 10), + lDevices: lDevices(element), + authentication: authentication(auth), + associations: associations(element), + }; + + return serverDescription; +} diff --git a/utils.ts b/utils.ts index 407f14ae..15193671 100644 --- a/utils.ts +++ b/utils.ts @@ -1,6 +1,7 @@ import { DADescription } from "./describe/DADescription.js"; import { DODescription } from "./describe/DODescription.js"; import { GSEControlDescription } from "./describe/GSEControl.js"; +import { LDeviceDescription } from "./describe/LDevice.js"; import { LNDescription } from "./describe/LN.js"; import { LogControlDescription } from "./describe/LogControl.js"; import { NamingDescription } from "./describe/Naming.js"; @@ -11,6 +12,7 @@ import { SampledValueControlDescription } from "./describe/SampledValueControl.j type SortedObjects = | DADescription | GSEControlDescription + | LDeviceDescription | LNDescription | LogControlDescription | NamingDescription