From ef038952e4869a5190d21a1f1f89d79b852d4a14 Mon Sep 17 00:00:00 2001 From: Jakob Vogelsang Date: Sat, 21 Oct 2023 00:57:22 +0200 Subject: [PATCH] feat: describe SampledValueControl --- describe/SampledValueControl.spec.ts | 116 +++++++++++++++++++++++++++ describe/SampledValueControl.ts | 104 ++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 describe/SampledValueControl.spec.ts create mode 100644 describe/SampledValueControl.ts diff --git a/describe/SampledValueControl.spec.ts b/describe/SampledValueControl.spec.ts new file mode 100644 index 00000000..18dd47ac --- /dev/null +++ b/describe/SampledValueControl.spec.ts @@ -0,0 +1,116 @@ +import { expect } from "chai"; + +import { describeSampledValueControl } from "./SampledValueControl.js"; + +const scl = new DOMParser().parseFromString( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IED3 + IED1 + IED2 + IED1 + R-GOOSE + + + + IED1 + IED2 + IED3 + IED1 + R-GOOSE + + + + + + + + + + + + + + + + + + + `, + "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)) + ); + }); +}); diff --git a/describe/SampledValueControl.ts b/describe/SampledValueControl.ts new file mode 100644 index 00000000..33075ad3 --- /dev/null +++ b/describe/SampledValueControl.ts @@ -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; +}