Skip to content

Commit 0f36a3d

Browse files
feat: Server
1 parent 0e4391a commit 0f36a3d

File tree

5 files changed

+318
-9
lines changed

5 files changed

+318
-9
lines changed

describe.spec.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ const testScl = new DOMParser().parseFromString(
88
xmlns:sxy="http://www.iec.ch/61850/2003/SCLcoordinates"
99
xmlns:ens="http://somevalidURI"
1010
>
11-
<IED name="IED">
11+
<IED name="IED1">
1212
<AccessPoint name="AP1">
1313
<Server>
14+
<Authentication />
1415
<LDevice inst="ldInst1">
1516
<LN0 lnClass="LLN0" inst="" lnType="baseLLN0"/>
1617
<LN lnClass="XCBR" inst="1" lnType="baseXCBR"/>
@@ -26,6 +27,40 @@ const testScl = new DOMParser().parseFromString(
2627
</Server>
2728
</AccessPoint>
2829
</IED>
30+
<IED name="IED2">
31+
<AccessPoint name="AP1">
32+
<Server>
33+
<Authentication />
34+
<LDevice inst="ldInst1">
35+
<LN0 lnClass="LLN0" inst="" lnType="baseLLN0"/>
36+
<LN lnClass="XCBR" inst="1" lnType="baseXCBR"/>
37+
</LDevice>
38+
<LDevice inst="ldInst2">
39+
<LN0 lnClass="LLN0" inst="" lnType="equalLLN0"/>
40+
<LN lnClass="XCBR" inst="1" lnType="equalXCBR"/>
41+
</LDevice>
42+
<LDevice inst="ldInst3">
43+
<LN0 lnClass="LLN0" inst="" lnType="diffLLN0"/>
44+
<LN lnClass="XCBR" inst="1" lnType="diffXCBR"/>
45+
</LDevice>
46+
</Server>
47+
</AccessPoint>
48+
</IED>
49+
<IED name="IED3">
50+
<AccessPoint name="AP1">
51+
<Server>
52+
<Authentication />
53+
<LDevice inst="ldInst1">
54+
<LN0 lnClass="LLN0" inst="" lnType="baseLLN0"/>
55+
<LN lnClass="XCBR" inst="1" lnType="baseXCBR"/>
56+
</LDevice>
57+
<LDevice inst="ldInst2">
58+
<LN0 lnClass="LLN0" inst="" lnType="equalLLN0"/>
59+
<LN lnClass="XCBR" inst="1" lnType="equalXCBR"/>
60+
</LDevice>
61+
</Server>
62+
</AccessPoint>
63+
</IED>
2964
<DataTypeTemplates>
3065
<LNodeType lnClass="LLN0" id="baseLLN0" >
3166
<DO name="Beh" type="someEqualDOType" />
@@ -114,9 +149,15 @@ const baseEnumType = testScl.querySelector("#someID")!;
114149
const diffEnumType = testScl.querySelector("#someDiffID")!;
115150
const equalEnumType = testScl.querySelector("#someOtherID")!;
116151

117-
const baseLDevice = testScl.querySelector(`LDevice[inst="ldInst1"]`)!;
118-
const equalLDevice = testScl.querySelector(`LDevice[inst="ldInst2"]`)!;
119-
const diffLDevice = testScl.querySelector(`LDevice[inst="ldInst3"]`)!;
152+
const baseServer = testScl.querySelector(
153+
`IED[name="IED1"] LDevice[inst="ldInst1"]`,
154+
)!;
155+
const equalServer = testScl.querySelector(
156+
`IED[name="IED2"] LDevice[inst="ldInst2"]`,
157+
)!;
158+
const diffServer = testScl.querySelector(
159+
`IED[name="IED2"] LDevice[inst="ldInst3"]`,
160+
)!;
120161

121162
describe("Describe SCL elements function", () => {
122163
it("returns undefined with missing describe function", () =>
@@ -136,12 +177,12 @@ describe("Describe SCL elements function", () => {
136177
));
137178

138179
it("returns same description with semantically equal LDevice's", () =>
139-
expect(JSON.stringify(describeSclElement(baseLDevice))).to.equal(
140-
JSON.stringify(describeSclElement(equalLDevice)),
180+
expect(JSON.stringify(describeSclElement(baseServer))).to.equal(
181+
JSON.stringify(describeSclElement(equalServer)),
141182
));
142183

143184
it("returns different description with unequal LDevice elements", () =>
144-
expect(JSON.stringify(describeSclElement(baseLDevice))).to.not.equal(
145-
JSON.stringify(describeSclElement(diffLDevice)),
185+
expect(JSON.stringify(describeSclElement(baseServer))).to.not.equal(
186+
JSON.stringify(describeSclElement(diffServer)),
146187
));
147188
});

describe.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LDevice, LDeviceDescription } from "./describe/LDevice.js";
77
import { LNodeType, LNodeTypeDescription } from "./describe/LNodeType.js";
88
import { LN, LNDescription } from "./describe/LN.js";
99
import { LN0, LN0Description } from "./describe/LN0.js";
10+
import { Server, ServerDescription } from "./describe/Server.js";
1011

1112
export type Description =
1213
| PrivateDescription
@@ -18,7 +19,8 @@ export type Description =
1819
| LNodeTypeDescription
1920
| LNDescription
2021
| LN0Description
21-
| LDeviceDescription;
22+
| LDeviceDescription
23+
| ServerDescription;
2224
const sclElementDescriptors: Partial<
2325
Record<string, (element: Element) => Description | undefined>
2426
> = {
@@ -31,6 +33,7 @@ const sclElementDescriptors: Partial<
3133
LN,
3234
LN0,
3335
LDevice,
36+
Server,
3437
};
3538

3639
export function describe(element: Element): Description | undefined {

describe/Server.spec.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { expect } from "chai";
2+
3+
import { Server } from "./Server.js";
4+
5+
const scl = new DOMParser().parseFromString(
6+
`<SCL xmlns="http://www.iec.ch/61850/2003/SCL" >
7+
<IED name="IED1">
8+
<AccessPoint name="AP1">
9+
<Server timeout="30">
10+
<Authentication />
11+
<LDevice inst="lDevice1">
12+
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
13+
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
14+
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
15+
</LDevice>
16+
<LDevice inst="lDevice2">
17+
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
18+
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
19+
</LDevice>
20+
<Association iedName="IED3" ldInst="lDevice2" lnClass="LLN0" lnInst="" kind="pre-established" associationId="someId" />
21+
<Association iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="1" kind="predefined" associationId="someId" />
22+
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
23+
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
24+
</Server>
25+
</AccessPoint>
26+
</IED>
27+
<IED name="IED2">
28+
<AccessPoint name="AP1">
29+
<Server>
30+
<Authentication none="true" password="false" weak="false" strong="false" certificate="false" />
31+
<LDevice inst="lDevice2">
32+
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
33+
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
34+
</LDevice>
35+
<LDevice inst="lDevice1">
36+
<LN0 lnClass="LLN0" inst="" lnType="LLN0" />
37+
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
38+
<LN lnClass="MMXU" inst="1" lnType="MMXU" />
39+
</LDevice>
40+
<Association iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="1" />
41+
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="1" kind="predefined" associationId="someId" />
42+
<Association iedName="IED3" ldInst="lDevice2" lnClass="LLN0" lnInst="" kind="pre-established" associationId="someId" />
43+
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
44+
<Association desc="" iedName="IED3" ldInst="lDevice2" lnClass="MMXU" lnInst="2" kind="predefined" associationId="someId" />
45+
</Server>
46+
</AccessPoint>
47+
</IED>
48+
<IED name="IED3">
49+
<AccessPoint name="AP1">
50+
<Server timeout="13">
51+
<Authentication none="false" password="true" weak="true" strong="true" certificate="true" />
52+
<LDevice inst="lDevice1">
53+
<LN0 lnClass="LLN0" inst="" lnType="LLN02" />
54+
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
55+
</LDevice>
56+
</Server>
57+
</AccessPoint>
58+
</IED>
59+
<IED name="IED4">
60+
<AccessPoint name="AP1">
61+
<Server timeout="13">
62+
<LDevice inst="lDevice1">
63+
<LN0 lnClass="LLN0" inst="" lnType="LLN02" />
64+
<LN lnClass="MMXU" inst="2" lnType="MMXU" />
65+
</LDevice>
66+
</Server>
67+
</AccessPoint>
68+
</IED>
69+
<DataTypeTemplates>
70+
<LNodeType id="LLN0" desc="desc" lnClass="LLN0">
71+
<DO name="Beh" type="BehENS"/>
72+
</LNodeType>
73+
<LNodeType id="MMXU" desc="desc" lnClass="MMXU">
74+
<DO name="A" type="WYE"/>
75+
</LNodeType>
76+
<DOType cdc="ENS" id="BehENS" >
77+
<DA name="stVal" bType="Enum" type="BehModKind" fc="ST" >
78+
<Val>off</Val>
79+
</DA>
80+
</DOType>
81+
<DOType id="WYE" cdc="WYE">
82+
<SDO name="phsA" type="CMV" />
83+
</DOType>
84+
<DOType id="CMV" cdc="CMV" >
85+
<DA name="cVal" bType="Struct" fc="MX" type="Vector"/>
86+
</DOType>
87+
<DAType id="Vector" >
88+
<BDA name="mag" bType="Struct" type="AnalogueValue" />
89+
</DAType>
90+
<DAType id="AnalogueValue" >
91+
<BDA name="f" bType="FLOAT32" >
92+
<Val sGroup="3">60.60</Val>
93+
<Val sGroup="1">10.10</Val>
94+
<Val sGroup="2">40.10</Val>
95+
</BDA>
96+
</DAType>
97+
<EnumType id="BehModKind" >
98+
<EnumVal ord="1">on</EnumVal>
99+
<EnumVal ord="3">test</EnumVal>
100+
<EnumVal ord="5">off</EnumVal>
101+
</EnumType>
102+
</DataTypeTemplates>
103+
</SCL>`,
104+
"application/xml",
105+
);
106+
107+
const baseServer = scl.querySelector('IED[name="IED1"] Server')!;
108+
const equalServer = scl.querySelector('IED[name="IED2"] Server')!;
109+
const diffServer = scl.querySelector('IED[name="IED3"] Server')!;
110+
const invalidServer = scl.querySelector('IED[name="IED4"] Server')!;
111+
112+
describe("Description for SCL schema type LDevice", () => {
113+
it("returns undefined with missing lnType attribute", () =>
114+
expect(Server(invalidServer)).to.be.undefined);
115+
116+
it("default timeout attribute if present", () =>
117+
expect(Server(equalServer)?.timeout).to.equal(30));
118+
119+
it("default timeout attribute if present", () =>
120+
expect(Server(diffServer)?.timeout).to.equal(13));
121+
122+
it("returns same description with semantically equal LDevice's", () =>
123+
expect(JSON.stringify(Server(baseServer))).to.equal(
124+
JSON.stringify(Server(equalServer)),
125+
));
126+
127+
it("returns different description with unequal LDevice elements", () =>
128+
expect(JSON.stringify(Server(baseServer))).to.not.equal(
129+
JSON.stringify(Server(diffServer)),
130+
));
131+
});

describe/Server.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { sortRecord } from "../utils.js";
2+
import { LDevice, LDeviceDescription } from "./LDevice.js";
3+
import { NamingDescription, describeNaming } from "./Naming.js";
4+
5+
function compareAssociations(a: Association, b: Association): number {
6+
const stringifiedA = JSON.stringify(a);
7+
const stringifiedB = JSON.stringify(b);
8+
9+
if (stringifiedA < stringifiedB) return -1;
10+
if (stringifiedA > stringifiedB) return 1;
11+
return 0;
12+
}
13+
14+
interface Authentication {
15+
/** Authentication attribute none defaulted to true */
16+
none: boolean;
17+
/** Authentication attribute password defaulted to false */
18+
password: boolean;
19+
/** Authentication attribute weak defaulted to false */
20+
weak: boolean;
21+
/** Authentication attribute strong defaulted to false */
22+
strong: boolean;
23+
/** Authentication attribute certificate defaulted to false */
24+
certificate: boolean;
25+
}
26+
27+
interface Association {
28+
desc?: string;
29+
iedName: string;
30+
ldInst: string;
31+
/** Association attribute prefix defaulted to empty string */
32+
prefix: string;
33+
/** Association attribute lnClass */
34+
lnClass: string;
35+
/** Association attribute lnInst */
36+
lnInst: string;
37+
/** Association attribute kind */
38+
kind: "pre-established" | "predefined";
39+
/** Association attribute associationId */
40+
associationId?: string;
41+
}
42+
43+
export interface ServerDescription extends NamingDescription {
44+
/** Server attribute timeout defaulted to 30 */
45+
timeout: number;
46+
/** Server child Authentication */
47+
authentication: Authentication;
48+
/** Server children LDevice */
49+
lDevices: Record<string, LDeviceDescription>;
50+
/** Server children Association */
51+
associations: Association[];
52+
}
53+
54+
function associations(element: Element): Association[] {
55+
const associations: Association[] = [];
56+
Array.from(element.children)
57+
.filter((child) => child.tagName === "Association")
58+
.forEach((assoc) => {
59+
const kind = assoc.getAttribute("kind");
60+
const associationId = assoc.getAttribute("associationId");
61+
const iedName = assoc.getAttribute("iedName");
62+
const ldInst = assoc.getAttribute("ldInst");
63+
const desc = assoc.getAttribute("desc") ?? "";
64+
const prefix = assoc.getAttribute("prefix") ?? "";
65+
const lnClass = assoc.getAttribute("lnClass");
66+
const lnInst = assoc.getAttribute("lnInst");
67+
68+
if (
69+
!kind ||
70+
!["pre-established", "predefined"].includes(kind) ||
71+
!iedName ||
72+
!ldInst ||
73+
!lnClass ||
74+
lnInst === null
75+
)
76+
return;
77+
78+
const association: Association = {
79+
kind: kind as "pre-established" | "predefined",
80+
desc,
81+
iedName,
82+
ldInst,
83+
prefix,
84+
lnClass,
85+
lnInst,
86+
};
87+
if (associationId) association.associationId = associationId;
88+
89+
associations.push(association);
90+
});
91+
92+
return associations.sort(compareAssociations);
93+
}
94+
95+
function authentication(element: Element): Authentication {
96+
return {
97+
none: element.getAttribute("none") === "false" ? false : true,
98+
password: element.getAttribute("password") === "true" ? true : false,
99+
weak: element.getAttribute("weak") === "true" ? true : false,
100+
strong: element.getAttribute("strong") === "true" ? true : false,
101+
certificate: element.getAttribute("certificate") === "true" ? true : false,
102+
};
103+
}
104+
105+
function lDevices(element: Element): Record<string, LDeviceDescription> {
106+
const unsortedLDevices: Record<string, LDeviceDescription> = {};
107+
Array.from(element.children)
108+
.filter((child) => child.tagName === "LDevice")
109+
.forEach((lDevice) => {
110+
const inst = lDevice.getAttribute("inst");
111+
const lDeviceDescription = LDevice(lDevice);
112+
if (inst && !unsortedLDevices[inst] && lDeviceDescription)
113+
unsortedLDevices[inst] = lDeviceDescription;
114+
});
115+
116+
return sortRecord(unsortedLDevices) as Record<string, LDeviceDescription>;
117+
}
118+
119+
export function Server(element: Element): ServerDescription | undefined {
120+
const auth = element.querySelector(":scope > Authentication");
121+
if (!auth) return;
122+
123+
const serverDescription: ServerDescription = {
124+
...describeNaming(element),
125+
timeout: parseInt(element.getAttribute("timeout") ?? "30", 10),
126+
lDevices: lDevices(element),
127+
authentication: authentication(auth),
128+
associations: associations(element),
129+
};
130+
131+
return serverDescription;
132+
}

utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DADescription } from "./describe/DADescription.js";
22
import { DODescription } from "./describe/DODescription.js";
33
import { GSEControlDescription } from "./describe/GSEControl.js";
4+
import { LDeviceDescription } from "./describe/LDevice.js";
45
import { LNDescription } from "./describe/LN.js";
56
import { LogControlDescription } from "./describe/LogControl.js";
67
import { NamingDescription } from "./describe/Naming.js";
@@ -11,6 +12,7 @@ import { SampledValueControlDescription } from "./describe/SampledValueControl.j
1112
type SortedObjects =
1213
| DADescription
1314
| GSEControlDescription
15+
| LDeviceDescription
1416
| LNDescription
1517
| LogControlDescription
1618
| NamingDescription

0 commit comments

Comments
 (0)