Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions describe.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ const testScl = new DOMParser().parseFromString(
xmlns:sxy="http://www.iec.ch/61850/2003/SCLcoordinates"
xmlns:ens="http://somevalidURI"
>
<IED name="IED">
<IED name="IED1">
<AccessPoint name="AP1">
<Server>
<Authentication />
<LDevice inst="ldInst1">
<LN0 lnClass="LLN0" inst="" lnType="baseLLN0"/>
<LN lnClass="XCBR" inst="1" lnType="baseXCBR"/>
Expand All @@ -26,6 +27,40 @@ const testScl = new DOMParser().parseFromString(
</Server>
</AccessPoint>
</IED>
<IED name="IED2">
<AccessPoint name="AP1">
<Server>
<Authentication />
<LDevice inst="ldInst1">
<LN0 lnClass="LLN0" inst="" lnType="baseLLN0"/>
<LN lnClass="XCBR" inst="1" lnType="baseXCBR"/>
</LDevice>
<LDevice inst="ldInst2">
<LN0 lnClass="LLN0" inst="" lnType="equalLLN0"/>
<LN lnClass="XCBR" inst="1" lnType="equalXCBR"/>
</LDevice>
<LDevice inst="ldInst3">
<LN0 lnClass="LLN0" inst="" lnType="diffLLN0"/>
<LN lnClass="XCBR" inst="1" lnType="diffXCBR"/>
</LDevice>
</Server>
</AccessPoint>
</IED>
<IED name="IED3">
<AccessPoint name="AP1">
<Server>
<Authentication />
<LDevice inst="ldInst1">
<LN0 lnClass="LLN0" inst="" lnType="baseLLN0"/>
<LN lnClass="XCBR" inst="1" lnType="baseXCBR"/>
</LDevice>
<LDevice inst="ldInst2">
<LN0 lnClass="LLN0" inst="" lnType="equalLLN0"/>
<LN lnClass="XCBR" inst="1" lnType="equalXCBR"/>
</LDevice>
</Server>
</AccessPoint>
</IED>
<DataTypeTemplates>
<LNodeType lnClass="LLN0" id="baseLLN0" >
<DO name="Beh" type="someEqualDOType" />
Expand Down Expand Up @@ -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", () =>
Expand All @@ -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)),
));
});
5 changes: 4 additions & 1 deletion describe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,7 +19,8 @@ export type Description =
| LNodeTypeDescription
| LNDescription
| LN0Description
| LDeviceDescription;
| LDeviceDescription
| ServerDescription;
const sclElementDescriptors: Partial<
Record<string, (element: Element) => Description | undefined>
> = {
Expand All @@ -31,6 +33,7 @@ const sclElementDescriptors: Partial<
LN,
LN0,
LDevice,
Server,
};

export function describe(element: Element): Description | undefined {
Expand Down
131 changes: 131 additions & 0 deletions describe/Server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { expect } from "chai";

import { Server } from "./Server.js";

const scl = new DOMParser().parseFromString(
`<SCL xmlns="http://www.iec.ch/61850/2003/SCL" >
<IED name="IED1">
<AccessPoint name="AP1">
<Server timeout="30">
<Authentication />
<LDevice inst="lDevice1">
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
</LDevice>
<LDevice inst="lDevice2">
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
</LDevice>
<Association iedName="IED3" ldInst="lDevice2" lnClass="LLN0" lnInst="" kind="pre-established" associationId="someId" />
<Association iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="1" kind="predefined" associationId="someId" />
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
</Server>
</AccessPoint>
</IED>
<IED name="IED2">
<AccessPoint name="AP1">
<Server>
<Authentication none="true" password="false" weak="false" strong="false" certificate="false" />
<LDevice inst="lDevice2">
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
</LDevice>
<LDevice inst="lDevice1">
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
</LDevice>
<Association iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="1" />
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="1" kind="predefined" associationId="someId" />
<Association iedName="IED3" ldInst="lDevice2" lnClass="LLN0" lnInst="" kind="pre-established" associationId="someId" />
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
</Server>
</AccessPoint>
</IED>
<IED name="IED3">
<AccessPoint name="AP1">
<Server timeout="13">
<Authentication none="false" password="true" weak="true" strong="true" certificate="true" />
<LDevice inst="lDevice1">
<LN0 lnClass="LLN0" inst="" lnType="LLN02" />
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
</LDevice>
</Server>
</AccessPoint>
</IED>
<IED name="IED4">
<AccessPoint name="AP1">
<Server timeout="13">
<LDevice inst="lDevice1">
<LN0 lnClass="LLN0" inst="" lnType="LLN02" />
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
</LDevice>
</Server>
</AccessPoint>
</IED>
<DataTypeTemplates>
<LNodeType id="LLN0" desc="desc" lnClass="LLN0">
<DO name="Beh" type="BehENS"/>
</LNodeType>
<LNodeType id="MMXU" desc="desc" lnClass="MMXU">
<DO name="A" type="WYE"/>
</LNodeType>
<DOType cdc="ENS" id="BehENS" >
<DA name="stVal" bType="Enum" type="BehModKind" fc="ST" >
<Val>off</Val>
</DA>
</DOType>
<DOType id="WYE" cdc="WYE">
<SDO name="phsA" type="CMV" />
</DOType>
<DOType id="CMV" cdc="CMV" >
<DA name="cVal" bType="Struct" fc="MX" type="Vector"/>
</DOType>
<DAType id="Vector" >
<BDA name="mag" bType="Struct" type="AnalogueValue" />
</DAType>
<DAType id="AnalogueValue" >
<BDA name="f" bType="FLOAT32" >
<Val sGroup="3">60.60</Val>
<Val sGroup="1">10.10</Val>
<Val sGroup="2">40.10</Val>
</BDA>
</DAType>
<EnumType id="BehModKind" >
<EnumVal ord="1">on</EnumVal>
<EnumVal ord="3">test</EnumVal>
<EnumVal ord="5">off</EnumVal>
</EnumType>
</DataTypeTemplates>
</SCL>`,
"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)),
));
});
132 changes: 132 additions & 0 deletions describe/Server.ts
Original file line number Diff line number Diff line change
@@ -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<string, LDeviceDescription>;
/** 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<string, LDeviceDescription> {
const unsortedLDevices: Record<string, LDeviceDescription> = {};
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<string, LDeviceDescription>;
}

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;
}
2 changes: 2 additions & 0 deletions utils.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,6 +12,7 @@ import { SampledValueControlDescription } from "./describe/SampledValueControl.j
type SortedObjects =
| DADescription
| GSEControlDescription
| LDeviceDescription
| LNDescription
| LogControlDescription
| NamingDescription
Expand Down