Skip to content

Commit a46559b

Browse files
feat(tDataTypeTemplates): updateLNodeType
1 parent f5365f5 commit a46559b

12 files changed

+1880
-149
lines changed

index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { updateBay } from "./tBay/updateBay.js";
1010
export { updateVoltageLevel } from "./tVoltageLevel/updateVoltageLevel.js";
1111
export { updateSubstation } from "./tSubstation/updateSubstation.js";
1212
export { removeProcessElement } from "./tSubstation/removeProcessElement.js";
13+
export { updateLnType } from "./tSubstation/updateLnType.js";
1314

1415
export { InsertIedOptions, insertIed } from "./tIED/insertIED.js";
1516
export { updateIED } from "./tIED/updateIED.js";
@@ -71,8 +72,7 @@ export {
7172
export { sourceControlBlock } from "./tExtRef/sourceControlBlock.js";
7273
export { isSubscribed } from "./tExtRef/isSubscribed.js";
7374

74-
export { importLNodeType } from "./tDataTypeTemplates/importLNodeType.js";
75-
export { lNodeTypeToSelection } from "./tDataTypeTemplates/lNodeTypeToSelection.js";
75+
7676

7777
export {
7878
LNodeDescription,
@@ -82,7 +82,10 @@ export {
8282

8383
export { insertSelectedLNodeType } from "./tDataTypeTemplates/insertSelectedLNodeType.js";
8484

85-
export {removeDataType, RemoveDataTypeOptions} from "./tDataTypeTemplates/removeDataType.js"
85+
export { removeDataType, RemoveDataTypeOptions } from "./tDataTypeTemplates/removeDataType.js"
86+
export { importLNodeType } from "./tDataTypeTemplates/importLNodeType.js";
87+
export { updateLNodeType } from "./tDataTypeTemplates/updateLNodeType.js";
88+
export { lNodeTypeToSelection } from "./tDataTypeTemplates/lNodeTypeToSelection.js";
8689

8790
export {
8891
Supervision,

tDataTypeTemplates/foundation.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
2+
3+
function describeEnumType(element: Element): { vals: Record<string, string> } {
4+
const vals: Record<string, string> = {};
5+
6+
const sortedEnumVals = Array.from(element.children)
7+
.filter((child) => child.tagName === "EnumVal")
8+
.sort(
9+
(v1, v2) =>
10+
parseInt(v1.getAttribute("ord")!, 10) -
11+
parseInt(v2.getAttribute("ord")!, 10),
12+
);
13+
for (const val of sortedEnumVals)
14+
vals[val.getAttribute("ord")!] = val.textContent ?? "";
15+
16+
return { vals };
17+
}
18+
19+
function describeDAType(element: Element): {
20+
bdas: Record<string, Record<string, string | null>>;
21+
} {
22+
const bdas: Record<string, Record<string, string | null>> = {};
23+
for (const bda of Array.from(element.children)
24+
.filter((child) => child.tagName === "BDA")
25+
.sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) {
26+
const [bType, type, dchg, dupd, qchg] = [
27+
"bType",
28+
"type",
29+
"dchg",
30+
"dupd",
31+
"qchg",
32+
].map((attr) => bda.getAttribute(attr));
33+
bdas[bda.getAttribute("name")!] = { bType, type, dchg, dupd, qchg };
34+
}
35+
return { bdas };
36+
}
37+
38+
function describeDOType(element: Element) {
39+
const sdos: Record<string, Record<string, string | null>> = {};
40+
for (const sdo of Array.from(element.children)
41+
.filter((child) => child.tagName === "SDO")
42+
.sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) {
43+
const [name, type, transient] = ["name", "type", "transient"].map((attr) =>
44+
sdo.getAttribute(attr),
45+
);
46+
sdos[name!] = { type, transient };
47+
}
48+
const das: Record<string, Record<string, string | null>> = {};
49+
for (const da of Array.from(element.children)
50+
.filter((child) => child.tagName === "DA")
51+
.sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) {
52+
const [name, fc, bType, type, dchg, dupd, qchg] = [
53+
"name",
54+
"fc",
55+
"bType",
56+
"type",
57+
"dchg",
58+
"dupd",
59+
"qchg",
60+
].map((attr) => da.getAttribute(attr));
61+
das[name!] = {
62+
fc,
63+
bType,
64+
type,
65+
dchg,
66+
dupd,
67+
qchg,
68+
};
69+
}
70+
return {
71+
sdos,
72+
das,
73+
cdc: element.getAttribute("cdc"),
74+
};
75+
}
76+
77+
function describeLNodeType(element: Element) {
78+
const dos: Record<string, Record<string, string | null>> = {};
79+
for (const doElement of Array.from(element.children)
80+
.filter((child) => child.tagName === "DO")
81+
.sort((c1, c2) => c1.outerHTML.localeCompare(c2.outerHTML))) {
82+
const [name, type, transient] = ["name", "type", "transient"].map((attr) =>
83+
doElement.getAttribute(attr),
84+
);
85+
dos[name!] = { type, transient };
86+
}
87+
return {
88+
dos,
89+
lnClass: element.getAttribute("lnClass"),
90+
};
91+
}
92+
93+
const typeDescriptions = {
94+
EnumType: describeEnumType,
95+
DAType: describeDAType,
96+
DOType: describeDOType,
97+
LNodeType: describeLNodeType,
98+
} as Partial<Record<string, (e: Element) => object>>;
99+
100+
function describeElement(element: Element): object {
101+
const describe = typeDescriptions[element.tagName]!;
102+
103+
return describe(element);
104+
}
105+
106+
export function hashElement(element: Element): string {
107+
/** A direct copy from www.github.com/openscd/open-scd-core/foundation/cyrb64.ts */
108+
109+
/**
110+
* Hashes `str` using the cyrb64 variant of
111+
* https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
112+
* @returns digest - a rather insecure hash, very quickly
113+
*/
114+
function cyrb64(str: string): string {
115+
/* eslint-disable no-bitwise */
116+
let h1 = 0xdeadbeef;
117+
let h2 = 0x41c6ce57;
118+
/* eslint-disable-next-line no-plusplus */
119+
for (let i = 0, ch; i < str.length; i++) {
120+
ch = str.charCodeAt(i);
121+
h1 = Math.imul(h1 ^ ch, 2654435761);
122+
h2 = Math.imul(h2 ^ ch, 1597334677);
123+
}
124+
h1 =
125+
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
126+
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
127+
h2 =
128+
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
129+
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
130+
return (
131+
(h2 >>> 0).toString(16).padStart(8, "0") +
132+
(h1 >>> 0).toString(16).padStart(8, "0")
133+
);
134+
/* eslint-enable no-bitwise */
135+
}
136+
137+
return cyrb64(JSON.stringify(describeElement(element)));
138+
}
139+
140+
export function isEqualNode(ours: Element, theirs: Element): boolean {
141+
return JSON.stringify(describeElement(ours)) === JSON.stringify(describeElement(theirs));
142+
}

tDataTypeTemplates/importLNodeType.spec.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { expect } from "chai";
22

3+
import { Insert, isInsert, isRemove } from "../foundation/utils.js";
4+
35
import { importLNodeType } from "./importLNodeType.js";
46
import {
57
baseDataTypes,
68
emptyBayTemplate,
79
competeBayTemplate,
810
invalidBayTemplate,
11+
hardUpdate,
912
} from "./importLNodeType.testfiles.js";
1013

1114
import { findElement } from "../foundation/helpers.test.js";
@@ -21,6 +24,10 @@ const mmxuLNodeType = findElement(
2124
baseDataTypes,
2225
'LNodeType[id="Dummy.MMXU"]'
2326
) as Element;
27+
const tctrHardUpdate = findElement(
28+
hardUpdate,
29+
'LNodeType[id="Dummy.TCTR"]'
30+
) as Element;
2431

2532
describe("Function to import LNodeType with its sub data", () => {
2633
it("is returning an empty string on invalid SCL files", () => {
@@ -30,14 +37,14 @@ describe("Function to import LNodeType with its sub data", () => {
3037
});
3138

3239
it("is inserting the LNodeType element itself when missing", () => {
33-
const edits = importLNodeType(tctrLNodeType, emptyTemplate);
40+
const edits = importLNodeType(tctrLNodeType, emptyTemplate) as Insert[];
3441

3542
expect(edits.length).to.equal(6);
3643
expect((edits[1].node as Element).tagName).to.equal(tctrLNodeType.tagName);
3744
});
3845

3946
it("is inserting DataTypeTemplate element when missing", () => {
40-
const edits = importLNodeType(tctrLNodeType, emptyTemplate);
47+
const edits = importLNodeType(tctrLNodeType, emptyTemplate) as Insert[];
4148

4249
expect(edits.length).to.equal(6);
4350
expect((edits[0].node as Element).tagName).to.equal("DataTypeTemplates");
@@ -50,14 +57,35 @@ describe("Function to import LNodeType with its sub data", () => {
5057
});
5158

5259
it("is checking for duplicate data types", () => {
53-
const edits = importLNodeType(tctrLNodeType, completeTemplate);
60+
const edits = importLNodeType(tctrLNodeType, completeTemplate) as Insert[];
5461

5562
expect(edits.length).to.equal(0);
5663
});
5764

5865
it("does not cut out data type from the base project", () => {
59-
const edits = importLNodeType(mmxuLNodeType, emptyTemplate);
66+
const edits = importLNodeType(mmxuLNodeType, emptyTemplate) as Insert[];
6067

6168
edits.forEach((edit) => expect(edit.node.isConnected).to.be.false);
6269
});
70+
71+
it("insert when not duplicate", () => {
72+
const edits = importLNodeType(tctrHardUpdate, completeTemplate) as Insert[];
73+
74+
expect(edits.length).to.equal(1);
75+
});
76+
77+
it("allows to overwrite existing LNodeType", () => {
78+
const edits1 = importLNodeType(tctrHardUpdate, completeTemplate, { overwrite: true }) as Insert[];
79+
80+
expect(edits1.length).to.equal(2);
81+
82+
expect(edits1[0]).to.satisfies(isInsert);
83+
expect(edits1[1]).to.satisfies(isRemove);
84+
85+
const edits2 = importLNodeType(tctrHardUpdate, completeTemplate, { overwrite: false }) as Insert[];
86+
87+
expect(edits2.length).to.equal(1);
88+
89+
expect(edits2[0]).to.satisfies(isInsert);
90+
});
6391
});

tDataTypeTemplates/importLNodeType.testfiles.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export const competeBayTemplate = `<SCL xmlns="http://www.iec.ch/61850/2003/SCL"
3333
<DAType id="someAnalogueValueINT32">
3434
<BDA name="i" bType="INT32" />
3535
</DAType>
36+
<DAType id="someAnalogueValueFLOAT32">
37+
<BDA name="i" bType="INT32" />
38+
</DAType>
3639
<EnumType id="BehaviourModeKind">
3740
<EnumVal ord="1">on</EnumVal>
3841
<EnumVal ord="2">blocked</EnumVal>
@@ -44,6 +47,45 @@ export const competeBayTemplate = `<SCL xmlns="http://www.iec.ch/61850/2003/SCL"
4447
</SCL>`;
4548
export const invalidBayTemplate = `<SomeInvalidSCL></SomeInvalidSCL>`;
4649

50+
export const hardUpdate = `<SCL xmlns="http://www.iec.ch/61850/2003/SCL" xmlns:esld="https://transpower.co.nz/SCL/SSD/SLD/v0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2007" revision="B" release="4">
51+
<Header id="BayTemplate"/>
52+
<Substation name="S1">
53+
<VoltageLevel name="V1" desc="" nomFreq="50" numPhases="3">
54+
<Voltage unit="V" multiplier="k">110</Voltage>
55+
<Bay name="B1" desc=""/>
56+
</VoltageLevel>
57+
</Substation>
58+
<DataTypeTemplates>
59+
<LNodeType lnClass="TCTR" id="Dummy.TCTR" desc="Current Transformer">
60+
<DO name="Beh" desc="ENS_Beh" type="OpenSCD_ENS_Beh"/>
61+
<DO name="AmpSv" type="Dummy.SAV"/>
62+
<DO name="MyBeh" type="OpenSCD_ENS_Beh" />
63+
</LNodeType>
64+
<DOType cdc="ENS" id="OpenSCD_ENS_Beh">
65+
<DA name="stVal" bType="Enum" dchg="true" fc="ST" type="BehaviourModeKind"/>
66+
<DA name="q" bType="Quality" qchg="true" fc="ST"/>
67+
<DA name="t" bType="Timestamp" fc="ST"/>
68+
</DOType>
69+
<DOType cdc="SAV" id="Dummy.SAV" desc="Sampled value">
70+
<DA fc="MX" name="instMag" bType="Struct" type="someAnalogueValueINT32"/>
71+
<DA fc="MX" qchg="true" name="q" bType="Quality"/>
72+
</DOType>
73+
<DAType id="someAnalogueValueINT32">
74+
<BDA name="i" bType="INT32" />
75+
</DAType>
76+
<DAType id="someAnalogueValueFLOAT32">
77+
<BDA name="i" bType="INT32" />
78+
</DAType>
79+
<EnumType id="BehaviourModeKind">
80+
<EnumVal ord="1">on</EnumVal>
81+
<EnumVal ord="2">blocked</EnumVal>
82+
<EnumVal ord="3">test</EnumVal>
83+
<EnumVal ord="4">test/blocked</EnumVal>
84+
<EnumVal ord="5">off</EnumVal>
85+
</EnumType>
86+
</DataTypeTemplates>
87+
</SCL>`;
88+
4789
export const baseDataTypes = `
4890
<SCL xmlns="http://www.iec.ch/61850/2003/SCL" version="2007" revision="B" release="4">
4991
<Header id="BaseDataTypes"/>

tDataTypeTemplates/importLNodeType.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { Insert, createElement } from "../foundation/utils.js";
1+
import { Edit, Insert, createElement } from "../foundation/utils.js";
22

33
import { getReference } from "../tBaseElement/getReference.js";
4+
import { isEqualNode } from "./foundation.js";
5+
6+
type ImportLNodeTypeOptions = {
7+
overwrite?: boolean;
8+
}
49

510
function removeDuplicates(inserts: Insert[]): Insert[] {
611
const uniqueInserts: Insert[] = [];
@@ -25,7 +30,7 @@ function insertDataType(
2530
const existingDataType = targetDataTypeTemplate.querySelector(
2631
`${dataType.tagName}[id="${dataType.getAttribute("id")}"] `
2732
);
28-
if (existingDataType && dataType.isEqualNode(existingDataType)) return;
33+
if (existingDataType && isEqualNode(dataType, existingDataType)) return;
2934

3035
const node = dataType.cloneNode(true);
3136
// const node = dataType;
@@ -112,8 +117,9 @@ function getDoTypes(parent: Element): Element[] {
112117
*/
113118
export function importLNodeType(
114119
lNodeType: Element,
115-
targetDoc: XMLDocument
116-
): Insert[] {
120+
targetDoc: XMLDocument,
121+
option: ImportLNodeTypeOptions = {}
122+
): Edit[] {
117123
const doc = lNodeType.ownerDocument;
118124
const targetScl = targetDoc.querySelector("SCL");
119125

@@ -134,8 +140,16 @@ export function importLNodeType(
134140
.filter((enumType) => !!enumType) as Element[]
135141
);
136142

137-
return insertDataTypes(
143+
const inserts = insertDataTypes(
138144
[lNodeType, ...doTypes, ...daTypes, ...enumTypes],
139145
targetScl
140146
);
147+
if (option.overwrite === undefined || option.overwrite === false) return inserts;
148+
149+
const duplicatedLNodeType = targetScl.querySelector(
150+
`:root > DataTypeTemplates > LNodeType[id="${lNodeType.getAttribute("id")}"]`
151+
);
152+
if (!duplicatedLNodeType) return inserts;
153+
154+
return [...inserts, { node: duplicatedLNodeType }]
141155
}

0 commit comments

Comments
 (0)