'use strict'; const { parseXML, childAttr, required, boolean, dateTime, duration, number, } = require('../parser'); const { create, fragment } = require('xmlbuilder2'); const xsiNs = 'http://www.w3.org/2001/XMLSchema-instance'; const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#'; const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07'; const emixNs = 'http://docs.oasis-open.org/ns/emix/2011/06'; const powerNs = 'http://docs.oasis-open.org/ns/emix/2011/06/power'; const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110'; const energyInteropPayloadsNs = 'http://docs.oasis-open.org/ns/energyinterop/201110/payloads'; const calendarNs = 'urn:ietf:params:xml:ns:icalendar-2.0'; const calendarStreamNs = 'urn:ietf:params:xml:ns:icalendar-2.0:stream'; const siScaleNs = 'http://docs.oasis-open.org/ns/emix/2011/06/siscale'; const eiTargetMappings = [ { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'endDeviceAsset', xmlChildElement: 'mrid', json: 'endDeviceAsset', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'aggregatedPnode', xmlChildElement: 'node', json: 'aggregatedPnode', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'meterAsset', xmlChildElement: 'mrid', json: 'meterAsset', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'pnode', xmlChildElement: 'node', json: 'pNode', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'serviceDeliveryPoint', xmlChildElement: 'node', json: 'serviceDeliveryPoint', }, { xmlNs: 'ei', xmlNsUri: energyInteropNs, xmlElement: 'groupID', json: 'groupId', }, { xmlNs: 'ei', xmlNsUri: energyInteropNs, xmlElement: 'groupName', json: 'groupName', }, { xmlNs: 'ei', xmlNsUri: energyInteropNs, xmlElement: 'resourceID', json: 'resourceId', }, { xmlNs: 'ei', xmlNsUri: energyInteropNs, xmlElement: 'venID', json: 'venId', }, { xmlNs: 'ei', xmlNsUri: energyInteropNs, xmlElement: 'partyID', json: 'partyId', }, ]; const itemBaseMappings = [ { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'powerReal', additionalAttributes: [ { xmlElement: 'powerAttributes', json: 'powerAttributes', }, ], json: 'powerReal', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'powerReactive', additionalAttributes: [ { xmlElement: 'powerAttributes', json: 'powerAttributes', }, ], json: 'powerReactive', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'powerApparent', additionalAttributes: [ { xmlElement: 'powerAttributes', json: 'powerAttributes', }, ], json: 'powerApparent', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'voltage', json: 'voltage', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'energyApparent', json: 'energyApparent', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'energyReactive', json: 'energyReactive', }, { xmlNs: 'power', xmlNsUri: powerNs, xmlElement: 'energyReal', json: 'energyReal', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'currencyPerKWh', json: 'currencyPerKWh', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'currencyPerKW', json: 'currencyPerKW', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'currencyPerThm', json: 'currencyPerThm', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'currency', json: 'currency', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'current', json: 'current', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'frequency', json: 'frequency', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'Therm', json: 'therm', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'temperature', json: 'temperature', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'pulseCount', json: 'pulseCount', }, { xmlNs: 'oadr', xmlNsUri: oadrNs, xmlElement: 'customUnit', json: 'customUnit', }, ]; function parseItemBase(x) { for (const itemBaseMapping of itemBaseMappings) { const { json, xmlElement, additionalAttributes } = itemBaseMapping; const itemBaseList = x[xmlElement]; if (itemBaseList) { const itemBase = itemBaseList[0]['$$']; const result = { type: json, description: required( childAttr(itemBase, 'itemDescription'), 'itemDescription', ), units: required(childAttr(itemBase, 'itemUnits'), 'itemUnits'), siScaleCode: required( childAttr(itemBase, 'siScaleCode'), 'siScaleCode', ), }; for (const additionalMapping of additionalAttributes || []) { const unNamespacedAdditionalAttribute = additionalMapping.xmlElement; const attributesList = itemBase[unNamespacedAdditionalAttribute]; if (attributesList) { const xmlAttributes = attributesList[0]['$$']; const attributes = {}; Object.keys(xmlAttributes).forEach(key => { attributes[key] = required(childAttr(xmlAttributes, key), key); }); result[additionalMapping.json] = attributes; } } return result; } } } function serializeItemBase(x) { for (const itemBaseMapping of itemBaseMappings) { const { xmlNs, xmlNsUri, json, xmlElement, additionalAttributes, } = itemBaseMapping; if (x.type === json) { const result = fragment(); const innerResult = result.ele(xmlNsUri, `${xmlNs}:${xmlElement}`); innerResult.ele(xmlNsUri, xmlNs + ':itemDescription').txt(x.description); innerResult.ele(xmlNsUri, xmlNs + ':itemUnits').txt(x.units); innerResult.ele(siScaleNs, 'scale:siScaleCode').txt(x.siScaleCode); if (additionalAttributes) { additionalAttributes.forEach(additionalAttribute => { const additionalResult = innerResult.ele( xmlNsUri, `${xmlNs}:${additionalAttribute.xmlElement}`, ); const additionalJson = x[additionalAttribute.json]; Object.keys(additionalJson).forEach(key => { additionalResult .ele(xmlNsUri, `${xmlNs}:${key}`) .txt(additionalJson[key]); }); }); } return result; } } } function parseEiResponse(response) { return { code: required(childAttr(response, 'responseCode'), 'responseCode'), description: childAttr(response, 'responseDescription'), requestId: required(childAttr(response, 'requestID'), 'requestID'), }; } function parseEventDescriptor(eventDescriptor) { const result = { eventId: required(childAttr(eventDescriptor, 'eventID'), 'eventID'), modificationNumber: required( number(childAttr(eventDescriptor, 'modificationNumber')), 'modificationNumber', ), marketContext: childAttr( eventDescriptor.eiMarketContext[0]['$$'], 'marketContext', ), createdDateTime: required( childAttr(eventDescriptor, 'createdDateTime'), 'createdDateTime', ), eventStatus: required( childAttr(eventDescriptor, 'eventStatus'), 'eventStatus', ), }; const testEvent = boolean(childAttr(eventDescriptor, 'testEvent')); if (testEvent != null) { result.testEvent = testEvent; } const modificationDateTime = childAttr( eventDescriptor, 'modificationDateTime', ); if (modificationDateTime != null) result.modificationDateTime = modificationDateTime; const modificationReason = childAttr(eventDescriptor, 'modificationReason'); if (modificationReason != null) result.modificationReason = modificationReason; const priority = number(childAttr(eventDescriptor, 'priority')); if (priority != null) result.priority = priority; const vtnComment = childAttr(eventDescriptor, 'vtnComment'); if (vtnComment != null) result.vtnComment = vtnComment; return result; } function serializeEventDescriptor(eventDescriptor) { const result = fragment(); const eventDescriptorResult = result.ele( energyInteropNs, 'ei:eventDescriptor', ); eventDescriptorResult .ele(energyInteropNs, 'ei:eventID') .txt(eventDescriptor.eventId) .up() .ele(energyInteropNs, 'ei:modificationNumber') .txt(eventDescriptor.modificationNumber) .up() .ele(energyInteropNs, 'ei:eiMarketContext') .ele(emixNs, 'emix:marketContext') .txt(eventDescriptor.marketContext) .up() .up() .ele(energyInteropNs, 'ei:createdDateTime') .txt(eventDescriptor.createdDateTime) .up() .ele(energyInteropNs, 'ei:eventStatus') .txt(eventDescriptor.eventStatus); if (eventDescriptor.testEvent != null) { eventDescriptorResult .ele(energyInteropNs, 'ei:testEvent') .txt(eventDescriptor.testEvent); } if (eventDescriptor.modificationDateTime != null) { eventDescriptorResult .ele(energyInteropNs, 'ei:modificationDateTime') .txt(eventDescriptor.modificationDateTime); } if (eventDescriptor.modificationReason != null) { eventDescriptorResult .ele(energyInteropNs, 'ei:modificationReason') .txt(eventDescriptor.modificationReason); } if (eventDescriptor.priority != null) { eventDescriptorResult .ele(energyInteropNs, 'ei:priority') .txt(eventDescriptor.priority); } if (eventDescriptor.vtnComment != null) { eventDescriptorResult .ele(energyInteropNs, 'ei:vtnComment') .txt(eventDescriptor.vtnComment); } return result; } function parseToleranceTolerateStartAfter(tolerance) { if (tolerance) { const tolerate = childAttr(tolerance['$$'], 'tolerate'); if (tolerate) { return duration(tolerate, 'startafter'); } } } function parsePayloadFloat(payloadFloatInput) { const payloadFloat = required( childAttr(payloadFloatInput, 'payloadFloat'), 'payloadFloat', )['$$']; return required(number(childAttr(payloadFloat, 'value')), 'value'); } function serializePayloadFloat(payloadFloat) { const result = fragment(); result .ele(energyInteropNs, 'ei:payloadFloat') .ele(energyInteropNs, 'ei:value') .txt(payloadFloat); return result; } function serializeSignalPayload(signalPayload) { const result = fragment(); result .ele(energyInteropNs, 'ei:signalPayload') .import(serializePayloadFloat(signalPayload)); return result; } function parseSignalPayloads(signalPayloads) { return signalPayloads.map(x => parsePayloadFloat(x['$$'])); } function serializeSignalPayloads(signalPayloads) { return signalPayloads.map(x => serializeSignalPayload(x)); } function parseEventSignalInterval(eventSignalInterval) { const result = { signalPayloads: parseSignalPayloads(eventSignalInterval.signalPayload), }; const durationValue = duration( childAttr(eventSignalInterval, 'duration'), 'duration', ); if (durationValue != null) result.duration = durationValue; const dtStartValue = dateTime( childAttr(eventSignalInterval, 'dtstart'), 'date-time', ); if (dtStartValue != null) { result.startDate = dtStartValue; } const uidHolder = childAttr(eventSignalInterval, 'uid'); if (uidHolder != null) { result.uid = required(childAttr(uidHolder['$$'], 'text')); } return result; } function serializeEventSignalInterval(eventSignalInterval) { const result = fragment(); const interval = result.ele(energyInteropNs, 'ei:interval'); if (eventSignalInterval.duration) { interval .ele(calendarNs, 'cal:duration') .import(serializeDuration(eventSignalInterval.duration)); } if (eventSignalInterval.startDate) { interval .ele(calendarNs, 'cal:dtstart') .import(serializeDateTime(eventSignalInterval.startDate)); } if (eventSignalInterval.uid) { interval .ele(calendarNs, 'cal:uid') .ele(calendarNs, 'cal:text') .txt(eventSignalInterval.uid); } serializeSignalPayloads(eventSignalInterval.signalPayloads).forEach(payload => interval.import(payload), ); return result; } function parseEventSignalIntervals(eventSignalIntervals) { if (!eventSignalIntervals) { return []; } return eventSignalIntervals['interval'].map(x => parseEventSignalInterval(x['$$']), ); } function serializeEventSignalIntervals(eventSignalIntervals) { return eventSignalIntervals.map(x => serializeEventSignalInterval(x)); } function parseEiTarget(eiTarget) { const result = {}; for (const eiTargetMapping of eiTargetMappings) { const unNamespacedAttribute = eiTargetMapping.xmlElement; if (eiTarget[unNamespacedAttribute]) { const eiTargetValue = eiTarget[unNamespacedAttribute]; let newValues; if (eiTargetMapping.xmlChildElement) { const unNamespacedChildAttribute = eiTargetMapping.xmlChildElement; newValues = eiTargetValue[0]['$$'][unNamespacedChildAttribute]; } else { newValues = eiTargetValue; } const existing = result[eiTargetMapping.json] || []; result[eiTargetMapping.json] = [...existing, ...newValues]; } } return result; } function serializeEiTarget(eiTarget) { const result = fragment(); const targetElement = result.ele(energyInteropNs, 'ei:eiTarget'); for (const eiTargetMapping of eiTargetMappings) { if (eiTarget[eiTargetMapping.json]) { eiTarget[eiTargetMapping.json].forEach(target => { const { xmlNs, xmlNsUri, xmlElement, xmlChildElement, } = eiTargetMapping; if (xmlChildElement) { targetElement .ele(xmlNsUri, `${xmlNs}:${xmlElement}`) .ele(xmlNsUri, `${xmlNs}:${xmlChildElement}`) .txt(target); } else { targetElement.ele(xmlNsUri, `${xmlNs}:${xmlElement}`).txt(target); } }); } } return result; } function parseEventSignal(eventSignal) { const result = { intervals: parseEventSignalIntervals(eventSignal['intervals'][0]['$$']), signalName: required(childAttr(eventSignal, 'signalName'), 'signalName'), signalType: required(childAttr(eventSignal, 'signalType'), 'signalType'), signalId: required(childAttr(eventSignal, 'signalID'), 'signalID'), }; const eiTarget = childAttr(eventSignal, 'eiTarget'); if (eiTarget != null) { result.target = parseEiTarget(eiTarget['$$']); } const currentValue = childAttr(eventSignal, 'currentValue'); if (currentValue != null) result.currentValue = parsePayloadFloat(currentValue['$$']); const itemBase = parseItemBase(eventSignal); if (itemBase != null) result.itemBase = itemBase; return result; } function serializeEventSignal(eventSignal) { const result = fragment(); const eiEventSignal = result.ele(energyInteropNs, 'ei:eiEventSignal'); const intervals = eiEventSignal.ele(calendarStreamNs, 'strm:intervals'); serializeEventSignalIntervals(eventSignal.intervals).forEach(interval => intervals.import(interval), ); eiEventSignal.ele(energyInteropNs, 'ei:signalID').txt(eventSignal.signalId); eiEventSignal .ele(energyInteropNs, 'ei:signalName') .txt(eventSignal.signalName); eiEventSignal .ele(energyInteropNs, 'ei:signalType') .txt(eventSignal.signalType); if (eventSignal.target) { eiEventSignal.import(serializeEiTarget(eventSignal.target)); } if (eventSignal.currentValue != null) { eiEventSignal .ele(energyInteropNs, 'ei:currentValue') .import(serializePayloadFloat(eventSignal.currentValue)); } if (eventSignal.itemBase != null) { eiEventSignal.import(serializeItemBase(eventSignal.itemBase)); } return result; } function parseEventBaseline(eventSignal) { const result = { startDate: required( dateTime(childAttr(eventSignal, 'dtstart'), 'date-time'), 'dtstart', ), duration: required( duration(childAttr(eventSignal, 'duration'), 'duration'), 'duration', ), intervals: parseEventSignalIntervals(eventSignal['intervals'][0]['$$']), baselineId: required(childAttr(eventSignal, 'baselineID'), 'baselineID'), baselineName: required( childAttr(eventSignal, 'baselineName'), 'baselineName', ), }; const itemBase = parseItemBase(eventSignal); if (itemBase != null) result.itemBase = itemBase; return result; } function serializeEventBaseline(eventBaseline) { const result = fragment(); const eiEventBaseline = result.ele(energyInteropNs, 'ei:eiEventBaseline'); eiEventBaseline .ele(energyInteropNs, 'ei:baselineID') .txt(eventBaseline.baselineId); eiEventBaseline .ele(energyInteropNs, 'ei:baselineName') .txt(eventBaseline.baselineName); if (eventBaseline.duration) { eiEventBaseline .ele(calendarNs, 'cal:duration') .import(serializeDuration(eventBaseline.duration)); } if (eventBaseline.startDate) { eiEventBaseline .ele(calendarNs, 'cal:dtstart') .import(serializeDateTime(eventBaseline.startDate)); } const intervals = eiEventBaseline.ele(calendarStreamNs, 'strm:intervals'); serializeEventSignalIntervals(eventBaseline.intervals).forEach(interval => intervals.import(interval), ); if (eventBaseline.itemBase != null) { eiEventBaseline.import(serializeItemBase(eventBaseline.itemBase)); } return result; } function parseEiEventSignals(eiEventSignals) { const wrappedEventSignals = eiEventSignals.eiEventSignal; const wrappedBaselines = eiEventSignals.eiEventBaseline; const result = {}; if (wrappedEventSignals) { result.event = wrappedEventSignals.map(x => parseEventSignal(x['$$'])); } if (wrappedBaselines) { result.baseline = wrappedBaselines.map(x => parseEventBaseline(x['$$'])); } return result; } function serializeEiEventSignals(eiEventSignals) { const eventSignals = eiEventSignals.event.map(x => serializeEventSignal(x)); const eventBaselines = eiEventSignals.baseline ? eiEventSignals.baseline.map(x => serializeEventBaseline(x)) : []; return [...eventSignals, ...eventBaselines]; } function parseEiActivePeriod(activePeriod) { const properties = activePeriod['properties'][0]['$$']; const result = { startDate: required( dateTime(childAttr(properties, 'dtstart'), 'date-time'), 'dtstart', ), duration: required( duration(childAttr(properties, 'duration'), 'duration'), 'duration', ), }; const toleranceTolerateStartAfter = parseToleranceTolerateStartAfter( childAttr(properties, 'tolerance'), ); if (toleranceTolerateStartAfter != null) { result.toleranceTolerateStartAfter = toleranceTolerateStartAfter; } const notificationDuration = duration( childAttr(properties, 'x-eiNotification'), 'duration', ); if (notificationDuration != null) { result.notificationDuration = notificationDuration; } const rampUpDuration = duration( childAttr(properties, 'x-eiRampUp'), 'duration', ); if (rampUpDuration != null) { result.rampUpDuration = rampUpDuration; } const recoveryDuration = duration( childAttr(properties, 'x-eiRecovery'), 'duration', ); if (recoveryDuration != null) { result.recoveryDuration = recoveryDuration; } return result; } function serializeDateTime(dateTime) { return fragment() .ele(calendarNs, 'cal:date-time') .txt(dateTime) .up(); } function serializeToleranceTolerateStartAfter(duration) { const result = fragment(); result .ele(calendarNs, 'cal:tolerance') .ele(calendarNs, 'cal:tolerate') .ele(calendarNs, 'cal:startafter') .txt(duration); return result; } function serializeActivePeriod(activePeriod) { const result = fragment(); const activePeriodResult = result.ele(energyInteropNs, 'ei:eiActivePeriod'); const properties = activePeriodResult.ele(calendarNs, 'cal:properties'); const components = activePeriodResult.ele(calendarNs, 'cal:components'); components.att(xsiNs, 'xsi:nil', 'true'); properties .ele(calendarNs, 'cal:dtstart') .import(serializeDateTime(activePeriod.startDate)) .up() .ele(calendarNs, 'cal:duration') .import(serializeDuration(activePeriod.duration)) .up(); if (activePeriod.toleranceTolerateStartAfter) { properties.import( serializeToleranceTolerateStartAfter( activePeriod.toleranceTolerateStartAfter, ), ); } if (activePeriod.notificationDuration) { properties .ele(energyInteropNs, 'ei:x-eiNotification') .import(serializeDuration(activePeriod.notificationDuration)); } if (activePeriod.rampUpDuration) { properties .ele(energyInteropNs, 'ei:x-eiRampUp') .import(serializeDuration(activePeriod.rampUpDuration)); } if (activePeriod.recoveryDuration) { properties .ele(energyInteropNs, 'ei:x-eiRecovery') .import(serializeDuration(activePeriod.recoveryDuration)); } return result; } function parseEiEvent(eiEvent) { return { eventDescriptor: parseEventDescriptor(eiEvent.eventDescriptor[0]['$$']), activePeriod: parseEiActivePeriod(eiEvent.eiActivePeriod[0]['$$']), signals: parseEiEventSignals(eiEvent.eiEventSignals[0]['$$']), target: parseEiTarget(eiEvent.eiTarget[0]['$$']), }; } function serializeEiEvent(eiEvent) { const result = fragment(); const eiEventResult = result.ele(energyInteropNs, 'ei:eiEvent'); eiEventResult.import(serializeEventDescriptor(eiEvent.eventDescriptor)); eiEventResult.import(serializeActivePeriod(eiEvent.activePeriod)); const eiEventSignals = eiEventResult.ele( energyInteropNs, 'ei:eiEventSignals', ); serializeEiEventSignals(eiEvent.signals).forEach(signal => eiEventSignals.import(signal), ); if (eiEvent.target) { eiEventResult.import(serializeEiTarget(eiEvent.target)); } return result; } function parseEvents(eventList) { const events = []; for (const wrappedEvent of eventList) { const event = wrappedEvent['$$']; const parsedEiEvent = parseEiEvent(event.eiEvent[0]['$$']); parsedEiEvent.responseRequired = childAttr(event, 'oadrResponseRequired'); events.push(parsedEiEvent); } return events; } async function parse(input) { const json = await parseXML(input); const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][ 'oadrDistributeEvent' ][0]['$$']; const { code, description, requestId: responseRequestId } = parseEiResponse( o['eiResponse'][0]['$$'], ); const result = { _type: 'oadrDistributeEvent', responseCode: code, responseDescription: description, responseRequestId: responseRequestId, }; if (code < 200 || code >= 300) { return result; } const payloadRequestId = childAttr(o, 'requestID'); if (payloadRequestId != null) result.requestId = payloadRequestId; const vtnId = childAttr(o, 'vtnID'); if (vtnId != null) result.vtnId = vtnId; const events = parseEvents(o['oadrEvent'] || []); result.events = events; return result; } function serializeEiResponse(code, description, requestId) { const descriptionFrag = description != null ? fragment() .ele(energyInteropNs, 'ei:responseDescription') .txt(description) : fragment(); return fragment() .ele(energyInteropNs, 'ei:responseCode') .txt(code) .up() .import(descriptionFrag) .ele(energyInteropPayloadsNs, 'pyld:requestID') .txt(requestId) .up(); } function serializeDuration(duration) { return duration != null ? fragment() .ele(calendarNs, 'cal:duration') .txt(duration) : fragment(); } function validate(obj) { if (obj.responseCode == null) { throw new Error('Missing responseCode'); } if (obj.responseRequestId == null) { throw new Error('Missing responseRequestId'); } } function serializeOadrEvent(event) { const result = fragment(); const oadrEvent = result.ele(oadrNs, 'oadr2b:oadrEvent'); oadrEvent.import(serializeEiEvent(event)); oadrEvent .ele(oadrNs, 'oadr2b:oadrResponseRequired') .txt(event.responseRequired); return result; } function serializeEvents(events) { const result = fragment(); events.forEach(event => { result.import(serializeOadrEvent(event)); }); return result; } function serialize(obj) { validate(obj); const vtnId = obj.vtnId != null ? fragment() .ele(energyInteropNs, 'ei:vtnID') .txt(obj.vtnId) : fragment(); const requestId = obj.requestId != null ? fragment() .ele(oadrPayloadNs, 'pyld:requestID') .txt(obj.requestId) : fragment(); const doc = create({ namespaceAlias: { ns: oadrPayloadNs, oadr2b: oadrNs, ei: energyInteropNs, pyld: energyInteropPayloadsNs, cal: calendarNs, }, }) .ele('@oadr2b', 'oadr2b:oadrPayload') .ele('oadr2b:oadrSignedObject') .ele('oadr2b:oadrDistributeEvent') .att('@ei', 'ei:schemaVersion', '2.0b') .ele('@ei', 'ei:eiResponse') .import( serializeEiResponse( obj.responseCode, obj.responseDescription, obj.responseRequestId, ), ) .up() .import(vtnId) .import(requestId) .import(serializeEvents(obj.events)) .doc(); return doc.end({ headless: true, prettyPrint: false }); } module.exports = { parse, serialize, };