Skip to content
Merged
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
43 changes: 43 additions & 0 deletions tIED/updateIED.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,47 @@ describe("Function to an update the IED name attributes and its referenced eleme
).filter((iedName) => iedName.textContent?.startsWith("newIedName"));
expect(after.length).to.equal(4);
});

it("updates object references without checking permissions", () => {
const sclDom = new DOMParser().parseFromString(scl, "application/xml");
const sub2 = sclDom.querySelector('IED[name="Subscriber2"]')!;

const edits = updateIED({
element: sub2,
attributes: { name: "newIedName" },
});

handleEdit(edits);

const after = Array.from(
sclDom.querySelectorAll(
'DOI[name^="InRef"] > DAI[name="setSrcRef"] > Val',
),
).filter((val) => val.textContent?.startsWith("newIedName"));

expect(after.length).to.equal(5);
});

it("updates object references and checks permissions", () => {
const sclDom = new DOMParser().parseFromString(scl, "application/xml");
const sub2 = sclDom.querySelector('IED[name="Subscriber2"]')!;

const edits = updateIED(
{
element: sub2,
attributes: { name: "newIedName" },
},
true,
);

handleEdit(edits);

const after = Array.from(
sclDom.querySelectorAll(
'DOI[name^="InRef"] > DAI[name="setSrcRef"] > Val',
),
).filter((val) => val.textContent?.startsWith("newIedName"));

expect(after.length).to.equal(4);
});
});
127 changes: 126 additions & 1 deletion tIED/updateIED.testfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export const scl = `<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007"
</Server>
</AccessPoint>
</IED>
<IED name="Subscriber2" desc="Subscriber1" manufacturer="Dummy">
<IED name="Subscriber2" desc="Subscriber2" manufacturer="Dummy">
<Services>
<SupSubscription maxGo="4" maxSv="4"/>
</Services>
Expand Down Expand Up @@ -215,7 +215,132 @@ export const scl = `<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007"
</DOI>
</LN>
</LDevice>
<LDevice inst="TestObjRef">
<LN lnType="MyCSWI" prefix="CB1" lnClass="CSWI" inst="1">
<DOI name="Pos">
<SDI name="Oper">
<DAI name="ctlVal"/>
<SDI name="origin">
<DAI name="orCat"/>
</SDI>
</SDI>
<DAI name="stVal"/>
<DAI name="ctlModel">
<Val>sbo-with-enhanced-security</Val>
</DAI>
<DAI name="sboTimeout">
<Val>10000</Val>
</DAI>
<DAI name="stSeld">
<Val>false</Val>
</DAI>
</DOI>
<DOI name="InRef1">
<DAI name="setSrcRef">
<Val>Subscriber2TestObjRef/CB1CILO1.EnaOpn.stVal</Val>
</DAI>
</DOI>
<DOI name="InRef2">
<DAI name="setSrcRef">
<Val>Subscriber2TestObjRef/CB2CILO1.EnaCls.stVal</Val>
</DAI>
</DOI>
<DOI name="InRef3">
<DAI name="setSrcRef">
<Val>Subscriber2TestObjRef/CB3CILO1.EnaOpn.stVal</Val>
</DAI>
</DOI>
<DOI name="InRef4">
<DAI name="setSrcRef" valKind="Conf" valImport="true">
<Val>Subscriber2TestObjRef/CB4CILO1.EnaCls.stVal</Val>
</DAI>
</DOI>
<DOI name="InRef5">
<DAI name="setSrcRef" valKind="Conf" valImport="true">
<Val>Subscriber2TestObjRef/CB5CILO1.EnaCls.stVal</Val>
</DAI>
</DOI>
<DOI name="InRef6">
<DAI name="setSrcRef" valKind="Conf" valImport="true">
<Val>SomeOtherIEDObjRef/CB5CILO1.EnaCls.stVal</Val>
</DAI>
</DOI>
<DOI name="InRef7">
<DAI name="setSrcRef" valKind="Conf" valImport="true">
</DAI>
</DOI>
<DOI name="InRef8">
<DAI name="setSrcRef" valKind="Conf" valImport="true">
<Val>Subscriber2TestObjRef/CB5CILO1.EnaCls.stVal</Val>
</DAI>
</DOI>
<DOI name="InRef9">
<DAI name="setSrcRef" valKind="Conf" valImport="true">
<Val>Subscriber2NotExistingObjRef/CB5CILO1.EnaCls.stVal</Val>
</DAI>
</DOI>
</LN>
</LDevice>
</Server>
</AccessPoint>
</IED>
<DataTypeTemplates>
<LNodeType id="MyCSWI" lnClass="CSWI">
<DO name="Beh" type="behENS"/>
<DO name="Loc" type="SPS_0"/>
<DO name="LocSta" type="SPC_0"/>
<DO name="OpOpn" type="ACT_0"/>
<DO name="OpCls" type="ACT_0"/>
<DO name="Pos" type="MyPos"/>
<DO name="InRef1" type="InRefDOType1"/>
<DO name="InRef2" type="InRefDOType2"/>
<DO name="InRef3" type="InRefDOType3"/>
<DO name="InRef4" type="InRefDOType45"/>
<DO name="InRef5" type="InRefDOType45"/>
<DO name="InRef6" type="InRefDOType3"/>
<DO name="InRef7" type="InRefDOType45"/>
<DO name="InRef8" type="InRefDOType6"/>
<DO name="InRef9" type="InRefDOType3"/>
</LNodeType>
<DOType id="InRefDOType1" cdc="ORG">
<DA name="setSrcRef" bType="ObjRef" valKind="RO" dchg="true" fc="SP"/>
</DOType>
<DOType id="InRefDOType2" cdc="ORG">
<DA name="setSrcRef" bType="ObjRef" valKind="RO" valImport="true" dchg="true" fc="SP"/>
</DOType>
<DOType id="InRefDOType3" cdc="ORG">
<DA name="setSrcRef" bType="ObjRef" valKind="Conf" valImport="true" dchg="true" fc="SP"/>
</DOType>
<DOType id="InRefDOType45" cdc="ORG">
<DA name="setSrcRef" bType="ObjRef" dchg="true" fc="SP"/>
</DOType>
<DOType id="InRefDOType6" cdc="ORG">
<!--Whoops, sorry - no DA! -->
<DA name="setSrcRefX" bType="ObjRef" dchg="true" fc="SP"/>
</DOType>
<DOType id="MyPos" cdc="DPC">
<DA name="SBOw" bType="Struct" type="SomeMissingType1" fc="CO"/>
<DA name="Oper" bType="Struct" type="SomeMissingType2" fc="CO"/>
<DA name="Cancel" bType="Struct" type="SomeMissingType3" fc="CO"/>
<DA name="stVal" bType="Dbpos" dchg="true" fc="ST"/>
<DA name="q" sAddr="1002" bType="Quality" qchg="true" fc="ST"/>
<DA name="t" bType="Timestamp" fc="ST"/>
<DA name="stSeld" bType="BOOLEAN" dchg="true" fc="ST">
<Val>false</Val>
</DA>
<DA name="ctlModel" bType="Enum" valKind="RO" type="CtlModelKindEnum" dchg="true" fc="CF"/>
<DA name="sboTimeout" bType="INT32U" valKind="RO" dchg="true" fc="CF">
<Val>10000</Val>
</DA>
<DA name="operTimeout" bType="INT32U" valKind="RO" dchg="true" fc="CF">
<Val>1000</Val>
</DA>
</DOType>
<EnumType id="CtlModelKindEnum">
<EnumVal ord="0">status-only</EnumVal>
<EnumVal ord="1">direct-with-normal-security</EnumVal>
<EnumVal ord="3">direct-with-enhanced-security</EnumVal>
<EnumVal ord="4">sbo-with-enhanced-security</EnumVal>
</EnumType>
</DataTypeTemplates>
</SCL>`;
121 changes: 118 additions & 3 deletions tIED/updateIED.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Edit, Update } from "../foundation/utils.js";
import { createElement, Edit, Update } from "../foundation/utils.js";

import { controlBlockObjRef } from "../tControl/controlBlockObjRef.js";

Expand Down Expand Up @@ -30,7 +30,11 @@ function updateIEDNameTextContent(

return [
{ node },
{ parent: iedName, node: document.createTextNode(newIedName), reference: null },
{
parent: iedName,
node: document.createTextNode(newIedName),
reference: null,
},
];
});
}
Expand Down Expand Up @@ -134,6 +138,114 @@ function updateIedNameAttributes(
});
}

function objectReferenceToIed(dai: Element, oldIedName: string): boolean {
const val = dai.querySelector(":scope > Val")!;
const valContent = val.textContent;

if (!valContent || !valContent?.startsWith(oldIedName)) return false;

const lDeviceName = valContent.slice(oldIedName.length).split("/")[0];
const ied = dai.closest("IED");

const hasLDevice = ied?.querySelector(
`:scope > AccessPoint > Server > LDevice[inst="${lDeviceName}"]`,
);
if (!hasLDevice) return false;

return true;
}

function canModifyDA(daiOrdaType: Element): boolean {
const valImport = daiOrdaType.getAttribute("valImport");
const valKind = daiOrdaType.getAttribute("valKind");
return (
valImport === "true" && valKind !== null && ["Conf", "RO"].includes(valKind)
);
}

function objRefDetails(
anyLn: Element,
doName: string,
daName: string,
): { bType?: string | null; canModify?: boolean } | undefined {
const doc = anyLn.ownerDocument;
const lNodeType = doc.querySelector(
`:root > DataTypeTemplates > LNodeType[id="${anyLn.getAttribute(
"lnType",
)}"]`,
);

let leaf: Element | null | undefined = lNodeType;

const dO: Element | null | undefined = leaf?.querySelector(
`DO[name="${doName}"], SDO[name="${doName}"]`,
);
leaf = doc.querySelector(
`:root > DataTypeTemplates > DOType[id="${dO?.getAttribute("type")}"]`,
);

const dA: Element | null | undefined = leaf?.querySelector(
`DA[name="${daName}"]`,
);
if (!dA) return undefined;

const bType = dA.getAttribute("bType");
const canModify = canModifyDA(dA);

return { bType, canModify };
}

/** Find references used by the IED with the basic type of object reference.
* Then check if they require replacement.
* This function does not process LGOS and LSVS GoCBRef as these are
* handled separately.
*/
function updateObjectReferences(
ied: Element,
oldIedName: string,
newIedName: string,
checkPermission = false,
): Edit[] {
const objRefCandidates = Array.from(
ied.querySelectorAll("LN DAI > Val, LN0 DAI > Val"),
).filter((val) => {
const dai = val.parentElement!;
const ln = dai.closest("LN, LN0");
const lnClass = ln?.getAttribute("lnClass");
const doiName = dai.closest("DOI, SDI")?.getAttribute("name");
const daiName = dai.getAttribute("name");
const isSupervision =
lnClass &&
doiName &&
["LGOS", "LSVS"].includes(lnClass) &&
["GoCBRef", "SvCBRef"].includes(doiName);
if (!ln || !doiName || !daiName || isSupervision) return false;

const objRefInfo = objRefDetails(ln, doiName, daiName);

return (
objRefInfo?.bType === "ObjRef" &&
(checkPermission === false ||
objRefInfo?.canModify ||
canModifyDA(dai)) &&
objectReferenceToIed(dai, oldIedName)
);
});

return objRefCandidates.flatMap((val) => {
const objRef = val.textContent!;
const textContent = `${newIedName}${objRef.slice(oldIedName.length)}`;

const newVal = createElement(val.ownerDocument, "Val", {});
newVal.textContent = textContent;

return [
{ node: val },
{ parent: val.parentElement!, node: newVal, reference: null },
];
});
}

/**
* Function to schema valid update name and other attribute(s) in IED element
* (rename IED)
Expand All @@ -145,9 +257,11 @@ function updateIedNameAttributes(
* 3. Updates IEDName elements text content
* ```
* @param update - IED element and attributes to be changed in the IED element
* @param checkPermission - Check permission before changing object references
* (other than supervision node GoCBRef values).
* @returns - Set of addition edits updating all references SCL elements
*/
export function updateIED(update: Update): Edit[] {
export function updateIED(update: Update, checkPermission = false): Edit[] {
if (update.element.tagName !== "IED") return [];
if (!update.attributes.name) return [update];

Expand All @@ -161,5 +275,6 @@ export function updateIED(update: Update): Edit[] {
...updateIedNameAttributes(ied, oldIedName, newIedName),
...updateSubscriptionSupervision(ied, oldIedName, newIedName),
...updateIEDNameTextContent(ied, oldIedName, newIedName),
...updateObjectReferences(ied, oldIedName, newIedName, checkPermission),
];
}