Skip to content

feat: describe SampledValueControl #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: describeGSEControl
Choose a base branch
from
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
116 changes: 116 additions & 0 deletions describe/SampledValueControl.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { expect } from "chai";

import { describeSampledValueControl } from "./SampledValueControl.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>
<SampledValueControl name="smvControl1" datSet="baseDataSet" confRev="1" smvID="smvID" multicast="true" securityEnable="Signature" smpRate="80" nofASDU="1" smpMod="SmpPerPeriod" >
<SmvOpts refreshTime="false" sampleSynchronized="true" sampleRate="false" dataSet="false" security="false" timestamp="false" synchSourceId="false" />
<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>
</SampledValueControl>
<SampledValueControl name="smvControl2" datSet="equalDataSet" confRev="1" smvID="smvID" securityEnable="Signature" smpRate="80" nofASDU="1" >
<SmvOpts />
<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>
</SampledValueControl>
<SampledValueControl name="smvControl5" datSet="diffDataSet" smpRate="80" nofASDU="1" multicast="false" >
<SmvOpts refreshTime="true" sampleSynchronized="false" sampleRate="true" dataSet="true" security="true" timestamp="true" synchSourceId="true" />
</SampledValueControl>
<SampledValueControl name="smvControl3" datSet="invalidDataSet" />
<SampledValueControl name="smvControl4" datSet="invalidReference" />
<SampledValueControl name="missingSmpRate" datSet="diffDataSet" nofASDU="1" >
<SmvOpts />
</SampledValueControl>
<SampledValueControl name="missingNofASDU" datSet="diffDataSet" smpRate="80" >
<SmvOpts />
</SampledValueControl>
<SampledValueControl name="missingSmpOpts" datSet="diffDataSet" smpRate="80" nofASDU="1" />
</LN0>
</LDevice>
</Server>
</AccessPoint>
</IED>
</SCL>`,
"application/xml"
);

const baseSampledValueControl = scl.querySelector(`*[datSet="baseDataSet"]`)!;
const equalSampledValueControl = scl.querySelector('*[datSet="equalDataSet"]')!;
const diffSampledValueControl = scl.querySelector('*[datSet="diffDataSet"]')!;
const invalidDataSet = scl.querySelector('*[datSet="invalidDataSet"]')!;
const invalidReference = scl.querySelector('*[datSet="invalidReference"]')!;
const missingSmpRate = scl.querySelector(
'SampledValueControl[name="missingSmpRate"]'
)!;
const missingNofASDU = scl.querySelector(
'SampledValueControl[name="missingNofASDU"]'
)!;
const missingSmpOpts = scl.querySelector(
'SampledValueControl[name="missingSmpOpts"]'
)!;

describe("Description for SCL schema type SampledValueControl", () => {
it("returns undefined when referenced DataSet is undefined", () =>
expect(describeSampledValueControl(invalidDataSet)).to.be.undefined);

it("returns undefined with missing referenced DataSet", () =>
expect(describeSampledValueControl(invalidReference)).to.be.undefined);

it("returns undefined with missing smpRate attribute", () =>
expect(describeSampledValueControl(missingSmpRate)).to.be.undefined);

it("returns undefined with missing nofASDU attribute", () =>
expect(describeSampledValueControl(missingNofASDU)).to.be.undefined);

it("returns undefined with missing SmpOpts child", () =>
expect(describeSampledValueControl(missingSmpOpts)).to.be.undefined);

it("returns same description with semantically equal SampledValueControl's", () =>
expect(
JSON.stringify(describeSampledValueControl(baseSampledValueControl))
).to.equal(
JSON.stringify(describeSampledValueControl(equalSampledValueControl))
));

it("returns different description with unequal SampledValueControl elements", () => {
expect(
JSON.stringify(describeSampledValueControl(baseSampledValueControl))
).to.not.equal(
JSON.stringify(describeSampledValueControl(diffSampledValueControl))
);
});
});
104 changes: 104 additions & 0 deletions describe/SampledValueControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
ControlWithIEDNameDescription,
describeControlWithIEDName,
} from "./ControlWithIEDName.js";

type SmvOpts = {
/** SmvOpts attribute refreshTime defaulted to false */
refreshTime: boolean;
/** SmvOpts attribute sampleSynchronized defaulted to true */
sampleSynchronized: boolean;
/** SmvOpts attribute sampleRate defaulted to false */
sampleRate: boolean;
/** SmvOpts attribute dataSet defaulted to false */
dataSet: boolean;
/** SmvOpts attribute security defaulted to false */
security: boolean;
/** SmvOpts attribute timestamp defaulted to false */
timestamp: boolean;
/** SmvOpts attribute synchSourceId defaulted to false */
synchSourceId: boolean;
};

function smvOpts(element: Element): SmvOpts | undefined {
const smvOpts = element.querySelector(":scope > SmvOpts");
if (!smvOpts) return;

const some: SmvOpts = {
refreshTime: smvOpts.getAttribute("refreshTime") === "true" ? true : false,
sampleSynchronized:
smvOpts.getAttribute("sampleSynchronized") === "false" ? false : true,
sampleRate: smvOpts.getAttribute("sampleRate") === "true" ? true : false,
dataSet: smvOpts.getAttribute("dataSet") === "true" ? true : false,
security: smvOpts.getAttribute("security") === "true" ? true : false,
timestamp: smvOpts.getAttribute("timestamp") === "true" ? true : false,
synchSourceId:
smvOpts.getAttribute("synchSourceId") === "true" ? true : false,
};

return some;
}

export interface SampledValueControlDescription
extends ControlWithIEDNameDescription {
/** SampledValueControl attribute multicast defaulted to true */
multicast: boolean;
/** SampledValueControl attribute smvID */
smvID: string;
/** SampledValueControl attribute smpRate*/
smpRate: number;
/** SampledValueControl attribute nofASDU */
nofASDU: number;
/** SampledValueControl attribute smpMod defaulted to "SmpPerPeriod" */
smpMod: "SmpPerPeriod" | "SmpPerSec" | "SecPerSmp";
/** SampleValueControl attribute securityEnable defaulted to "None" */
securityEnable: "None" | "Signature" | "SignatureAndEncryption";
/** SampledValueControl child Protocol */
protocol?: { mustUnderstand: true; val: "R-SV" };
/** SampledValueControl child SmvOpts */
SmvOpts: SmvOpts;
}

export function describeSampledValueControl(
element: Element
): SampledValueControlDescription | undefined {
const controlWithTriggerOptDesc = describeControlWithIEDName(element);
if (!controlWithTriggerOptDesc) return;

const smpRate = element.getAttribute("smpRate");
if (!smpRate || isNaN(parseInt(smpRate, 10))) return;

const nofASDU = element.getAttribute("nofASDU");
if (!nofASDU || isNaN(parseInt(nofASDU, 10))) return;

const SmvOpts = smvOpts(element);
if (!SmvOpts) return;

const gseControlDescription: SampledValueControlDescription = {
...controlWithTriggerOptDesc,
multicast: element.getAttribute("multicast") === "false" ? false : true,
smvID: element.getAttribute("smvID") ?? "",
smpRate: parseInt(smpRate, 10),
nofASDU: parseInt(nofASDU, 10),
smpMod: element.getAttribute("smpMod")
? (element.getAttribute("smpMod") as
| "SecPerSmp"
| "SmpPerSec"
| "SmpPerPeriod")
: "SmpPerPeriod",
securityEnable: element.getAttribute("securityEnable")
? (element.getAttribute("securityEnable") as
| "Signature"
| "SignatureAndEncryption")
: "None",
SmvOpts: SmvOpts,
};

const protocol = Array.from(element.children).find(
(child) => child.tagName === "Protocol"
);
if (protocol)
gseControlDescription.protocol = { mustUnderstand: true, val: "R-SV" };

return gseControlDescription;
}