forked from openscd/open-scd
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #342 from com-pas/feat/334-104-export-menu-plugin
feat: 104 Export menu plugin closes #334
- Loading branch information
Showing
7 changed files
with
1,211 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { LitElement, property } from 'lit-element'; | ||
import { stringify } from 'csv-stringify/browser/esm/sync'; | ||
import { newLogEvent } from '@openscd/core/foundation/deprecated/history.js'; | ||
|
||
import { extractAllSignal104Data, Signal104 } from './export104/foundation.js'; | ||
import { get } from 'lit-translate'; | ||
|
||
|
||
|
||
export default class Export104 extends LitElement { | ||
@property({ attribute: false }) doc!: XMLDocument; | ||
@property() docName!: string; | ||
|
||
private readonly csvHeaders = [ | ||
'Id', | ||
'Name', | ||
'Signal Number', | ||
'mIOA', | ||
'cIOA' | ||
]; | ||
|
||
async run(): Promise<void> { | ||
const { signals, errors } = extractAllSignal104Data(this.doc); | ||
|
||
errors.forEach((error) => this.logWarning(error)); | ||
|
||
if (signals.length === 0) { | ||
this.dispatchEvent(newLogEvent({ | ||
kind: 'info', | ||
title: get('protocol104.export.noSignalsFound'), | ||
})); | ||
return; | ||
} | ||
|
||
const csvLines = this.generateCsvLines(signals); | ||
|
||
const csvContent = stringify(csvLines, { | ||
header: true, | ||
columns: this.csvHeaders, | ||
}); | ||
const csvBlob = new Blob([csvContent], { | ||
type: 'text/csv', | ||
}); | ||
|
||
this.downloadCsv(csvBlob); | ||
} | ||
|
||
private logWarning(errorMessage: string): void { | ||
this.dispatchEvent(newLogEvent({ | ||
kind: 'warning', | ||
title: get('protocol104.export.invalidSignalWarning'), | ||
message: errorMessage, | ||
})); | ||
} | ||
|
||
private generateCsvLines(allSignal104Data: Signal104[]): string[][] { | ||
const lines: string[][] = []; | ||
|
||
for(const signal104Data of allSignal104Data) { | ||
const line = [ | ||
'', | ||
signal104Data.name ?? '', | ||
signal104Data.signalNumber ?? '', | ||
]; | ||
|
||
if (signal104Data.isMonitorSignal) { | ||
line.push(signal104Data.ioa ?? '', ''); | ||
} else { | ||
line.push('', signal104Data.ioa ?? ''); | ||
} | ||
|
||
lines.push(line); | ||
} | ||
|
||
return lines; | ||
} | ||
|
||
private downloadCsv(csvBlob: Blob): void { | ||
const a = document.createElement('a'); | ||
a.download = this.docName + '-104-signals.csv'; | ||
a.href = URL.createObjectURL(csvBlob); | ||
a.dataset.downloadurl = ['text/csv', a.download, a.href].join(':'); | ||
a.style.display = 'none'; | ||
|
||
document.body.appendChild(a); | ||
a.click(); | ||
document.body.removeChild(a); | ||
URL.revokeObjectURL(a.href); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { get } from "lit-translate"; | ||
|
||
export const PROTOCOL_104_PRIVATE = 'IEC_60870_5_104'; | ||
|
||
export interface Signal104 { | ||
name: string | null; | ||
signalNumber: string | null; | ||
isMonitorSignal: boolean; | ||
ioa: string | null; | ||
ti: string | null; | ||
} | ||
|
||
interface ExtractSignal104Result { | ||
signal: Signal104 | null; | ||
error?: string; | ||
} | ||
|
||
enum SignalType { | ||
Monitor, | ||
Control, | ||
Unknown | ||
} | ||
|
||
const private104Selector = `Private[type="${PROTOCOL_104_PRIVATE}"]`; | ||
|
||
export function extractAllSignal104Data(doc: XMLDocument): { signals: Signal104[], errors: string[] } { | ||
const signals: Signal104[] = []; | ||
const errors: string[] = []; | ||
const address104Elements = doc.querySelectorAll(`${private104Selector} > Address`); | ||
|
||
address104Elements.forEach((addressElement) => { | ||
const signal104Result = extractSignal104Data(addressElement, doc); | ||
|
||
if (signal104Result.error) { | ||
errors.push(signal104Result.error); | ||
} else { | ||
signals.push(signal104Result.signal!); | ||
} | ||
}); | ||
|
||
return { signals, errors }; | ||
} | ||
|
||
function extractSignal104Data(addressElement: Element, doc: XMLDocument): ExtractSignal104Result { | ||
const ti = addressElement.getAttribute('ti'); | ||
const ioa = addressElement.getAttribute('ioa'); | ||
|
||
// By convention the last four digits of the ioa are the signalnumber, see https://github.com/com-pas/compas-open-scd/issues/334 | ||
if (ti === null || ioa === null || ioa.length < 4) { | ||
return { signal: null, error: get('protocol104.export.errors.tiOrIoaInvalid', { ti: ti ?? '', ioa: ioa ?? '' }) }; | ||
} | ||
const { signalNumber, bayName } = splitIoa(ioa); | ||
|
||
const signalType = getSignalType(ti); | ||
if (signalType === SignalType.Unknown) { | ||
return { signal: null, error: get('protocol104.export.errors.unknownSignalType', { ti: ti ?? '', ioa: ioa ?? '' }) }; | ||
} | ||
const isMonitorSignal = signalType === SignalType.Monitor; | ||
|
||
addressElement.parentElement; | ||
const parentDOI = addressElement.closest('DOI'); | ||
|
||
if (!parentDOI) { | ||
return { signal: null, error: get('protocol104.export.errors.noDoi', { ioa: ioa ?? '' }) }; | ||
} | ||
|
||
const doiDesc = parentDOI.getAttribute('desc'); | ||
|
||
const parentBayQuery = `:root > Substation > VoltageLevel > Bay[name="${bayName}"]`; | ||
const parentBay = doc.querySelector(parentBayQuery); | ||
|
||
if (!parentBay) { | ||
return { signal: null, error: get('protocol104.export.errors.noBay', { bayName, ioa: ioa ?? '' }) }; | ||
} | ||
|
||
const parentVoltageLevel = parentBay.closest('VoltageLevel'); | ||
|
||
if (!parentVoltageLevel) { | ||
return { signal: null, error: get('protocol104.export.errors.noVoltageLevel', { bayName, ioa: ioa ?? '' }) }; | ||
} | ||
|
||
const voltageLevelName = parentVoltageLevel.getAttribute('name'); | ||
const parentSubstation = parentVoltageLevel.closest('Substation'); | ||
|
||
if (!parentSubstation) { | ||
return { signal: null, error: get('protocol104.export.errors.noSubstation', { voltageLevelName: voltageLevelName ?? '', ioa: ioa ?? '' }) }; | ||
} | ||
|
||
const substationName = parentSubstation.getAttribute('name'); | ||
|
||
const name = `${substationName}${voltageLevelName}${bayName}${doiDesc}`; | ||
|
||
return { | ||
signal: { | ||
name, | ||
signalNumber, | ||
isMonitorSignal, | ||
ti, | ||
ioa | ||
} | ||
} | ||
} | ||
|
||
// For signal classification details see https://github.com/com-pas/compas-open-scd/issues/334 | ||
function getSignalType(tiString: string): SignalType { | ||
const ti = parseInt(tiString); | ||
|
||
if (isNaN(ti)) { | ||
return SignalType.Unknown; | ||
} | ||
|
||
if ((ti >= 1 && ti <= 21) || (ti >= 30 && ti <= 40)) { | ||
return SignalType.Monitor; | ||
} else if ((ti >= 45 && ti <= 51) || (ti >= 58 && ti <= 64)) { | ||
return SignalType.Control; | ||
} else { | ||
return SignalType.Unknown; | ||
} | ||
} | ||
|
||
// By Alliander convention the last four digits of the ioa are the signalnumber and the rest is the bay number | ||
// And every bay name consists of "V" + bay number | ||
function splitIoa(ioa: string): { signalNumber: string, bayName: string } { | ||
const signalNumber = ioa.slice(-4); | ||
const bayName = `V${ioa.slice(0, -4)}`; | ||
|
||
return { signalNumber, bayName }; | ||
} |
Oops, something went wrong.