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
80 changes: 80 additions & 0 deletions describe/ControlWithIEDName.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect } from "chai";

import { describeControlWithIEDName } from "./ControlWithIEDName.js";

const scl = new DOMParser().parseFromString(
`<SCL xmlns="http://www.iec.ch/61850/2003/SCL" >
<IED name="IED1">
<AccessPoint name="AP1">
<Server>
<LDevice inst="lDevice">
<LN0 lnClass="LLN0" inst="" lnType="LLN0">
<DataSet name="baseDataSet" >
<FCDA iedName="IED1" ldInst="lDevice" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="q" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="LLN0" doName="Beh" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="LLN0" lnInst="" doName="Beh" fc="ST" />
</DataSet>
<DataSet name="equalDataSet" >
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="XCBR" lnInst="1" doName="Pos" daName="q" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="LLN0" lnInst="" doName="Beh" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="LLN0" doName="Beh" fc="ST" />
</DataSet>
<DataSet name="diffDataSet" >
<Private type="private" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="XCBR" lnInst="1" doName="Pos" daName="q" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="LLN0" lnInst="" doName="Beh" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="LLN0" doName="Beh" fc="ST" />
</DataSet>
<DataSet name="invalidDataSet" >
<FCDA ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
</DataSet>
<GSEControl name="gseControl1" datSet="baseDataSet" confRev="1" >
<IEDName apRef="AP1" ldInst="ldInst" prefix="CB" lnClass="CSWI" lnInst="1">IED3</IEDName>
<IEDName >IED1</IEDName>
<IEDName apRef="AP1" ldInst="ldInst" lnClass="LLN0">IED2</IEDName>
<IEDName >IED1</IEDName>
</GSEControl>
<GSEControl name="gseControl2" datSet="equalDataSet" confRev="1" >
<IEDName >IED1</IEDName>
<IEDName apRef="AP1" ldInst="ldInst" prefix="" lnClass="LLN0" lnInst="" >IED2</IEDName>
<IEDName apRef="AP1" ldInst="ldInst" prefix="CB" lnClass="CSWI" lnInst="1" >IED3</IEDName>
<IEDName >IED1</IEDName>
</GSEControl>
<GSEControl name="gseControl5" datSet="diffDataSet" />
<GSEControl name="gseControl3" datSet="invalidDataSet" />
<GSEControl name="gseControl4" datSet="invalidReference" />
</LN0>
</LDevice>
</Server>
</AccessPoint>
</IED>
</SCL>`,
"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))));
});
80 changes: 80 additions & 0 deletions describe/ControlWithIEDName.ts
Original file line number Diff line number Diff line change
@@ -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;
}
82 changes: 82 additions & 0 deletions describe/GSEControl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { expect } from "chai";

import { describeGSEControl } from "./GSEControl.js";

const scl = new DOMParser().parseFromString(
`<SCL xmlns="http://www.iec.ch/61850/2003/SCL" >
<IED name="IED1">
<AccessPoint name="AP1">
<Server>
<LDevice inst="lDevice">
<LN0 lnClass="LLN0" inst="" lnType="LLN0">
<DataSet name="baseDataSet" >
<FCDA iedName="IED1" ldInst="lDevice" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="q" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="LLN0" doName="Beh" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="LLN0" lnInst="" doName="Beh" fc="ST" />
</DataSet>
<DataSet name="equalDataSet" >
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="XCBR" lnInst="1" doName="Pos" daName="q" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="LLN0" lnInst="" doName="Beh" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="LLN0" doName="Beh" fc="ST" />
</DataSet>
<DataSet name="diffDataSet" >
<Private type="private" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="XCBR" lnInst="1" doName="Pos" daName="q" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" prefix="" lnClass="LLN0" lnInst="" doName="Beh" daName="stVal" fc="ST" />
<FCDA iedName="IED1" ldInst="lDevice" lnClass="LLN0" doName="Beh" fc="ST" />
</DataSet>
<DataSet name="invalidDataSet" >
<FCDA ldInst="lDevice" prefix="" lnClass="XCBR" lnInst="1" doName="Pos" daName="stVal" fc="ST" />
</DataSet>
<GSEControl name="gseControl1" datSet="baseDataSet" confRev="1" appID="appID" fixedOffs="false" securityEnable="Signature" >
<IEDName apRef="AP1" ldInst="ldInst" prefix="CB" lnClass="CSWI" lnInst="1">IED3</IEDName>
<IEDName >IED1</IEDName>
<IEDName apRef="AP1" ldInst="ldInst" lnClass="LLN0">IED2</IEDName>
<IEDName >IED1</IEDName>
<Protocol mustUnderstand="true">R-GOOSE</Protocol>
</GSEControl>
<GSEControl name="gseControl2" datSet="equalDataSet" confRev="1" appID="appID" securityEnable="Signature" >
<IEDName >IED1</IEDName>
<IEDName apRef="AP1" ldInst="ldInst" prefix="" lnClass="LLN0" lnInst="" >IED2</IEDName>
<IEDName apRef="AP1" ldInst="ldInst" prefix="CB" lnClass="CSWI" lnInst="1" >IED3</IEDName>
<IEDName >IED1</IEDName>
<Protocol mustUnderstand="true">R-GOOSE</Protocol>
</GSEControl>
<GSEControl name="gseControl5" datSet="diffDataSet" type="GSSE" fixedOffs="true" />
<GSEControl name="gseControl3" datSet="invalidDataSet" />
<GSEControl name="gseControl4" datSet="invalidReference" />
</LN0>
</LDevice>
</Server>
</AccessPoint>
</IED>
</SCL>`,
"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))
));
});
44 changes: 44 additions & 0 deletions describe/GSEControl.ts
Original file line number Diff line number Diff line change
@@ -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;
}