|
| 1 | +import { LitElement, property } from 'lit-element'; |
| 2 | +import { createElement, newActionEvent } from '../../../foundation.js'; |
| 3 | + |
| 4 | +let cbNum = 1; |
| 5 | +let dsNum = 1; |
| 6 | + |
| 7 | +//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard |
| 8 | +// Check on a later point if implementation of an export of this function will remain valid. |
| 9 | +function addLNodes(condEq: Element, cswi: Element): Element { |
| 10 | + // switchgear ideally is a composition of lnClass CILO,CSWI,XSWI |
| 11 | + cswi.parentElement |
| 12 | + ?.querySelectorAll( |
| 13 | + `LN[lnClass="CSWI"]${ |
| 14 | + cswi.getAttribute('prefix') |
| 15 | + ? `[prefix="${cswi.getAttribute('prefix')}"]` |
| 16 | + : `` |
| 17 | + }${ |
| 18 | + cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : `` |
| 19 | + },LN[lnClass="CILO"]${ |
| 20 | + cswi.getAttribute('prefix') |
| 21 | + ? `[prefix="${cswi.getAttribute('prefix')}"]` |
| 22 | + : `` |
| 23 | + }${ |
| 24 | + cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : `` |
| 25 | + },LN[lnClass="XCBR"]${ |
| 26 | + cswi.getAttribute('prefix') |
| 27 | + ? `[prefix="${cswi.getAttribute('prefix')}"]` |
| 28 | + : `` |
| 29 | + }${ |
| 30 | + cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : `` |
| 31 | + },LN[lnClass="XSWI"]${ |
| 32 | + cswi.getAttribute('prefix') |
| 33 | + ? `[prefix="${cswi.getAttribute('prefix')}"]` |
| 34 | + : `` |
| 35 | + }${ |
| 36 | + cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : `` |
| 37 | + }` |
| 38 | + ) |
| 39 | + .forEach(ln => { |
| 40 | + condEq.appendChild( |
| 41 | + createElement(cswi.ownerDocument, 'LNode', { |
| 42 | + iedName: |
| 43 | + ln.parentElement?.parentElement?.parentElement?.parentElement?.getAttribute( |
| 44 | + 'name' |
| 45 | + ) ?? null, |
| 46 | + ldInst: cswi.parentElement?.getAttribute('inst') ?? null, |
| 47 | + prefix: ln.getAttribute('prefix'), |
| 48 | + lnClass: ln.getAttribute('lnClass'), |
| 49 | + lnInst: ln.getAttribute('inst'), |
| 50 | + }) |
| 51 | + ); |
| 52 | + }); |
| 53 | + |
| 54 | + return condEq; |
| 55 | +} |
| 56 | + |
| 57 | +//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard |
| 58 | +// Check on a later point if implementation of an export of this function will remain valid. |
| 59 | +function getSwitchGearType(cswi: Element): string { |
| 60 | + return cswi.parentElement?.querySelector( |
| 61 | + `LN[lnClass="XCBR"]${ |
| 62 | + cswi.getAttribute('prefix') |
| 63 | + ? `[prefix="${cswi.getAttribute('prefix')}"]` |
| 64 | + : `` |
| 65 | + }${ |
| 66 | + cswi.getAttribute('inst') ? `[inst="${cswi.getAttribute('inst')}"]` : `` |
| 67 | + }` |
| 68 | + ) |
| 69 | + ? 'CBR' |
| 70 | + : 'DIS'; |
| 71 | +} |
| 72 | + |
| 73 | +//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard |
| 74 | +// Check on a later point if implementation of an export of this function will remain valid. |
| 75 | +function getSwitchGearName(ln: Element): string { |
| 76 | + if (ln.getAttribute('prefix') && ln.getAttribute('inst')) |
| 77 | + return ln.getAttribute('prefix')! + ln.getAttribute('inst'); |
| 78 | + |
| 79 | + if (ln.getAttribute('inst') && getSwitchGearType(ln) === 'CBR') |
| 80 | + return 'QA' + cbNum++; |
| 81 | + |
| 82 | + return 'QB' + dsNum++; |
| 83 | +} |
| 84 | + |
| 85 | +//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard |
| 86 | +// Check on a later point if implementation of an export of this function will remain valid. |
| 87 | +function isSwitchGear(ln: Element, selectedCtlModel: string[]): boolean { |
| 88 | + // ctlModel can be configured in IED section. |
| 89 | + if ( |
| 90 | + Array.from( |
| 91 | + ln.querySelectorAll('DOI[name="Pos"] > DAI[name="ctlModel"] > Val') |
| 92 | + ).filter(val => selectedCtlModel.includes(val.innerHTML.trim())).length |
| 93 | + ) |
| 94 | + return true; |
| 95 | + |
| 96 | + // ctlModel can be configured as type in DataTypeTemplate section |
| 97 | + const doc = ln.ownerDocument; |
| 98 | + return ( |
| 99 | + Array.from( |
| 100 | + doc.querySelectorAll( |
| 101 | + `DataTypeTemplates > LNodeType[id="${ln.getAttribute( |
| 102 | + 'lnType' |
| 103 | + )}"] > DO[name="Pos"]` |
| 104 | + ) |
| 105 | + ) |
| 106 | + .map(DO => (<Element>DO).getAttribute('type')) |
| 107 | + .flatMap(doType => |
| 108 | + Array.from( |
| 109 | + doc.querySelectorAll( |
| 110 | + `DOType[id="${doType}"] > DA[name="ctlModel"] > Val` |
| 111 | + ) |
| 112 | + ) |
| 113 | + ) |
| 114 | + .filter(val => selectedCtlModel.includes((<Element>val).innerHTML.trim())) |
| 115 | + .length > 0 |
| 116 | + ); |
| 117 | +} |
| 118 | + |
| 119 | +//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard |
| 120 | +// Check on a later point if implementation of an export of this function will remain valid. |
| 121 | +function getCSWI(ied: Element): Element[] { |
| 122 | + return Array.from( |
| 123 | + ied.querySelectorAll('AccessPoint > Server > LDevice > LN[lnClass="CSWI"]') |
| 124 | + ); |
| 125 | +} |
| 126 | + |
| 127 | +//TODO : Got this from guess-wizard, it's unclear if this functionality will stay the same in the guess wizard |
| 128 | +// Check on a later point if implementation of an export of this function will remain valid. |
| 129 | +function getValidCSWI(ied: Element, selectedCtlModel: string[]): Element[] { |
| 130 | + if (!ied.parentElement) return []; |
| 131 | + |
| 132 | + return getCSWI(ied).filter(cswi => isSwitchGear(cswi, selectedCtlModel)); |
| 133 | +} |
| 134 | + |
| 135 | +export default class CompasAutogenerateSubstation extends LitElement { |
| 136 | + @property() doc!: XMLDocument; |
| 137 | + @property() iedNames!: string[]; |
| 138 | + @property() substationSeperator = '__'; |
| 139 | + @property() voltageLevelNameLength = 3; |
| 140 | + @property() iedStartChar = 'A'; |
| 141 | + |
| 142 | + async run(): Promise<void> { |
| 143 | + //Get the lNodes inside the document that are already linked to and IED |
| 144 | + const lNodes = this.extractNames( |
| 145 | + Array.from(this.doc.querySelectorAll('LNode')).map( |
| 146 | + value => value.getAttribute('iedName') || '' |
| 147 | + ) |
| 148 | + ); |
| 149 | + |
| 150 | + //Get name attribute from all the IEDs that aren't linked in the document |
| 151 | + this.iedNames = Array.from(this.doc.querySelectorAll('IED')) |
| 152 | + .map(IED => IED.getAttribute('name') || '') |
| 153 | + .filter(value => !lNodes.includes(value)); |
| 154 | + |
| 155 | + //Get all the substation names by splitting the names on the '__' (seperator) and getting the characters in front of it |
| 156 | + const substationNames = this.extractNames( |
| 157 | + this.iedNames.map(FullName => |
| 158 | + FullName.includes(this.substationSeperator) |
| 159 | + ? FullName?.split(this.substationSeperator)[0] |
| 160 | + : '' |
| 161 | + ) |
| 162 | + ); |
| 163 | + this.createSubstations(substationNames); |
| 164 | + } |
| 165 | + |
| 166 | + /** |
| 167 | + * Creating substations based on the list of names. First check if a substation with that name already exists. |
| 168 | + * If the substation element doesn't exist yet, a substation element will be created with the given name and a default |
| 169 | + * description. |
| 170 | + * |
| 171 | + * The created substation element with its name will be used to create voltageLevels as child elements to the substations. |
| 172 | + * Afterwards the substation elements will be added to the document. |
| 173 | + */ |
| 174 | + createSubstations(substationNames: string[]) { |
| 175 | + substationNames.forEach(async name => { |
| 176 | + if (this.doc.querySelector(`Substation[name=${name}]`) === null) { |
| 177 | + const desc = 'Substation generated by CoMPAS'; |
| 178 | + const substation = createElement(this.doc, 'Substation', { |
| 179 | + name, |
| 180 | + desc, |
| 181 | + }); |
| 182 | + |
| 183 | + await this.createVoltageLevels(substation, name); |
| 184 | + |
| 185 | + this.dispatchEvent( |
| 186 | + newActionEvent({ |
| 187 | + new: { |
| 188 | + parent: this.doc.querySelector('SCL')!, |
| 189 | + element: substation, |
| 190 | + }, |
| 191 | + }) |
| 192 | + ); |
| 193 | + } |
| 194 | + }); |
| 195 | + } |
| 196 | + |
| 197 | + /** |
| 198 | + * The name-content of the child elements will be extracted by splitting the ied name on the substationSeperator ('__' by default) |
| 199 | + * character and getting the characters after it. |
| 200 | + * VoltageLevel elements will be created by getting the first voltageLevelNameLength characters of each element in the name content. |
| 201 | + * The elements will be created based on the name and some default values. |
| 202 | + * |
| 203 | + * Afterwards the first voltageLevelNameLength of characters will be filtered out of the name content and the remaining content |
| 204 | + * will be used to create bay elements. |
| 205 | + * The elements will be appended to the substation element |
| 206 | + * @param substation substation(parent) element |
| 207 | + * @param substationName name of the substation |
| 208 | + */ |
| 209 | + createVoltageLevels(substation: Element, substationName: string) { |
| 210 | + const substationContent = this.iedNames |
| 211 | + .filter(value => value.includes(substationName)) |
| 212 | + .map(FullName => FullName?.split(this.substationSeperator)[1]); |
| 213 | + |
| 214 | + const voltageLevelNames = this.extractNames( |
| 215 | + substationContent.map(FullName => |
| 216 | + FullName?.substring(0, this.voltageLevelNameLength) |
| 217 | + ) |
| 218 | + ); |
| 219 | + |
| 220 | + voltageLevelNames.forEach(name => { |
| 221 | + const desc = 'Voltage Level generated by CoMPAS'; |
| 222 | + const nomFreq = '50.0'; |
| 223 | + const numPhases = '3'; |
| 224 | + const voltageLevel = createElement( |
| 225 | + substation.ownerDocument, |
| 226 | + 'VoltageLevel', |
| 227 | + { |
| 228 | + name, |
| 229 | + desc, |
| 230 | + nomFreq, |
| 231 | + numPhases, |
| 232 | + } |
| 233 | + ); |
| 234 | + |
| 235 | + const voltageLevelContent = substationContent |
| 236 | + .filter(value => value.startsWith(name)) |
| 237 | + .map(FullName => |
| 238 | + FullName?.substring(this.voltageLevelNameLength, FullName.length) |
| 239 | + ); |
| 240 | + |
| 241 | + this.createBays( |
| 242 | + voltageLevel, |
| 243 | + voltageLevelContent, |
| 244 | + substationName + this.substationSeperator + name |
| 245 | + ); |
| 246 | + substation.appendChild(voltageLevel); |
| 247 | + }); |
| 248 | + } |
| 249 | + |
| 250 | + /** |
| 251 | + * Bay elements will be created by getting the characters before the IED start character of each element in the remaining name content. |
| 252 | + * Afterwards the IED names that contain in the bay will be determined by filtering out the Voltage Level content that start with the bayname. |
| 253 | + * The IED element will be found by doing a query with the full IED name (substationVoltageLevelName + IED name). |
| 254 | + * The IED element will be used the create LNode elements. The guessLNodes will create elements based on if the IED contains switchgear. |
| 255 | + * If the element doesn't contain switchgear a deafult LNode element will be created as child and appended to the bay. |
| 256 | + * |
| 257 | + * @param voltageLevel voltageLevel(parent) element |
| 258 | + * @param voltageLevelContent remaining content extracted from the IEDs (name without substation name and voltageLevel name) |
| 259 | + * @param substationVoltageLevelName The name of the substation + voltageLevel |
| 260 | + */ |
| 261 | + createBays( |
| 262 | + voltageLevel: Element, |
| 263 | + voltageLevelContent: string[], |
| 264 | + substationVoltageLevelName: string |
| 265 | + ) { |
| 266 | + const bayNames = this.extractNames( |
| 267 | + voltageLevelContent.map(iedName => iedName.split(this.iedStartChar)[0]) |
| 268 | + ); |
| 269 | + |
| 270 | + bayNames.forEach(name => { |
| 271 | + const desc = 'Bay generated by CoMPAS'; |
| 272 | + const bayElement = createElement(voltageLevel.ownerDocument, 'Bay', { |
| 273 | + name, |
| 274 | + desc, |
| 275 | + }); |
| 276 | + |
| 277 | + const iedNames = voltageLevelContent.filter(value => |
| 278 | + value.startsWith(name) |
| 279 | + ); |
| 280 | + iedNames.forEach(iedName => { |
| 281 | + const currentIed = this.doc.querySelector( |
| 282 | + `IED[name=${substationVoltageLevelName + iedName}]` |
| 283 | + ); |
| 284 | + |
| 285 | + const guessLNodes = this.createLNodeElements(currentIed!, [ |
| 286 | + 'sbo-with-enhanced-security', |
| 287 | + ]); |
| 288 | + |
| 289 | + guessLNodes |
| 290 | + ? guessLNodes.forEach(lNode => bayElement.appendChild(lNode)) |
| 291 | + : bayElement.prepend(this.addDefaultLNodes(currentIed!)); |
| 292 | + }); |
| 293 | + voltageLevel.appendChild(bayElement); |
| 294 | + }); |
| 295 | + } |
| 296 | + |
| 297 | + /** |
| 298 | + * Create ConductingEquipment with LNode children based on whether the IED contains switchgear |
| 299 | + * @param ied |
| 300 | + * @param ctlModelList |
| 301 | + * @returns ConductingEquipment with LNode child elements or null |
| 302 | + */ |
| 303 | + createLNodeElements(ied: Element, ctlModelList: string[]): Element[] | null { |
| 304 | + const switchGear = getValidCSWI(ied, ctlModelList); |
| 305 | + |
| 306 | + if (switchGear.length) { |
| 307 | + const condEq = switchGear.map(cswi => { |
| 308 | + return addLNodes( |
| 309 | + createElement(ied.ownerDocument, 'ConductingEquipment', { |
| 310 | + name: getSwitchGearName(cswi), |
| 311 | + type: getSwitchGearType(cswi), |
| 312 | + }), |
| 313 | + cswi |
| 314 | + ); |
| 315 | + }); |
| 316 | + |
| 317 | + return condEq; |
| 318 | + } |
| 319 | + return null; |
| 320 | + } |
| 321 | + |
| 322 | + /** |
| 323 | + * Create default LNode element with default values |
| 324 | + * @param currentIed |
| 325 | + * @returns LNode element |
| 326 | + */ |
| 327 | + addDefaultLNodes(currentIed: Element): Element { |
| 328 | + const ln = currentIed.querySelector('LN0'); |
| 329 | + |
| 330 | + return createElement(currentIed.ownerDocument, 'LNode', { |
| 331 | + iedName: currentIed.getAttribute('name'), |
| 332 | + ldInst: currentIed.parentElement?.getAttribute('inst') ?? null, |
| 333 | + prefix: ln?.getAttribute('prefix') ?? null, |
| 334 | + lnClass: ln?.getAttribute('lnClass') ?? null, |
| 335 | + lnInst: ln?.getAttribute('inst') ?? null, |
| 336 | + }); |
| 337 | + } |
| 338 | + |
| 339 | + /** |
| 340 | + * Helper function to filter out empty and duplicate elements |
| 341 | + * @param content |
| 342 | + * @returns filtered content |
| 343 | + */ |
| 344 | + extractNames(content: string[]) { |
| 345 | + //Check for empty elements |
| 346 | + const elementNames = content.filter(Boolean); |
| 347 | + |
| 348 | + //return list of names after filtering out the duplicate elements |
| 349 | + return elementNames.filter( |
| 350 | + (value, index) => value && elementNames.indexOf(<string>value) === index |
| 351 | + ); |
| 352 | + } |
| 353 | +} |
0 commit comments