Skip to content

Commit

Permalink
Merge pull request #342 from com-pas/feat/334-104-export-menu-plugin
Browse files Browse the repository at this point in the history
feat: 104 Export menu plugin

closes #334
  • Loading branch information
clepski authored Oct 14, 2024
2 parents b5c0740 + 8f1b555 commit c72d953
Show file tree
Hide file tree
Showing 7 changed files with 1,211 additions and 0 deletions.
9 changes: 9 additions & 0 deletions packages/compas-open-scd/public/js/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,13 @@ export const officialPlugins = [
requireDoc: true,
position: 'middle',
},
{
name: 'Export IEC 104 CSV',
src: '/plugins/src/menu/Export104.js',
icon: 'sim_card_download',
default: false,
kind: 'menu',
requireDoc: true,
position: 'middle',
},
];
12 changes: 12 additions & 0 deletions packages/compas-open-scd/src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,18 @@ export const de: Translations = {
scaleMultiplierHelper: '???',
scaleOffsetHelper: '???',
},
export: {
noSignalsFound: 'Export 104 hat keine Signale gefunden',
invalidSignalWarning: 'Export 104 hat ein ungültiges Signal gefunden',
errors: {
tiOrIoaInvalid: 'ti or ioa fehlen oder ioa hat weniger als 4 Zeichen, ti: "{{ ti }}", ioa: "{{ ioa }}"',
unknownSignalType: 'Unbekannter Signaltyp für ti: "{{ ti }}", ioa: "{{ ioa }}"',
noDoi: 'Es wurde kein Eltern DOI Element gefunden für ioa: "{{ ioa }}"',
noBay: 'Es wurde kein Bay Element mit dem Namen "{{ bayName }}" für ioa: "{{ ioa }}" gefunden',
noVoltageLevel: 'Es wurde kein VoltageLevel Element für Bay "{{ bayName }}" gefunden für ioa "{{ ioa }}"',
noSubstation: 'Es wurde kein Substation Element gefunden für VoltageLevel "{{ voltageLevelName }}" für ioa "{{ ioa }}"'
}
}
},
'compare-ied': {
selectProjectTitle: 'Lade IEDs aus Vorlage',
Expand Down
12 changes: 12 additions & 0 deletions packages/compas-open-scd/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,18 @@ export const en = {
scaleMultiplierHelper: 'Scale Multiplier',
scaleOffsetHelper: 'Scale Offset',
},
export: {
noSignalsFound: 'Export 104 found no signals',
invalidSignalWarning: 'Export 104 found invalid signal',
errors: {
tiOrIoaInvalid: 'ti or ioa are missing or ioa is less than 4 digits, ti: "{{ ti }}", ioa: "{{ ioa }}"',
unknownSignalType: 'Unknown signal type for ti: "{{ ti }}", ioa: "{{ ioa }}"',
noDoi: 'No parent DOI found for address with ioa: "{{ ioa }}"',
noBay: 'No Bay found bayname: "{{ bayName }}" for address with ioa: "{{ ioa }}"',
noVoltageLevel: 'No parent voltage level found for bay "{{ bayName }}" for ioa "{{ ioa }}"',
noSubstation: 'No parent substation found for voltage level "{{ voltageLevelName }}" for ioa "{{ ioa }}"'
}
}
},
'compare-ied': {
selectProjectTitle: 'Select template project to Compare IED with',
Expand Down
90 changes: 90 additions & 0 deletions packages/plugins/src/menu/Export104.ts
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);
}
}
128 changes: 128 additions & 0 deletions packages/plugins/src/menu/export104/foundation.ts
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 };
}
Loading

0 comments on commit c72d953

Please sign in to comment.