'use strict'; const logger = require('../logger'); const nantum = require('../modules/nantum'); const { vtnId } = require('../config'); function calculateEventStatus( startDate, durationSeconds, notificationDurationSeconds, rampUpDurationSeconds, cancelled, ) { if (cancelled) return 'cancelled'; const nowMillis = new Date().getTime(); const startMillis = new Date(startDate).getTime(); const endMillis = startMillis + durationSeconds * 1000; const notificationStartMillis = startMillis - (notificationDurationSeconds || 0) * 1000; const rampStartMillis = startMillis - (rampUpDurationSeconds || 0) * 1000; if (nowMillis < notificationStartMillis) { return 'none'; } if (nowMillis < startMillis) { if (nowMillis < rampStartMillis) { return 'far'; } return 'near'; } if (nowMillis < endMillis) { return 'active'; } return 'completed'; } function calculateOadrDuration(seconds) { if (seconds == null) return; return `PT${seconds}S`; } function calculateEventIntervals(intervals) { return intervals.map(interval => { return { signalPayloads: interval.signal_payloads, duration: calculateOadrDuration(interval.duration_seconds), uid: interval.uid, }; }); } function calculateItemBase(itemBase) { if (!itemBase) return; return { type: itemBase.type, description: itemBase.dis, units: itemBase.units, siScaleCode: itemBase.si_scale_code, powerAttributes: itemBase.power_attributes, }; } function calculateTargets(targets) { return targets.map(target => { return { type: target.target_type, value: target.value, }; }); } function calculateEventSignals(signals) { return signals.map(signal => { return { signalName: signal.signal_name, signalId: signal.signal_id, signalType: signal.signal_type, currentValue: signal.current_value, duration: calculateOadrDuration(signal.duration_seconds), startDate: signal.start_date, intervals: calculateEventIntervals(signal.intervals), itemBase: calculateItemBase(signal.item_base), }; }); } function calculateBaselineSignal(nantumBaseline) { if (!nantumBaseline) return; return { baselineName: nantumBaseline.baseline_name, baselineId: nantumBaseline.baseline_id, duration: calculateOadrDuration(nantumBaseline.duration_seconds), startDate: nantumBaseline.start_date, intervals: calculateEventIntervals(nantumBaseline.intervals), }; } function convertToOadrEvent(event) { return { eventDescriptor: { eventId: event._id, modificationNumber: event.modification_number, modificationDateTime: event.modification_date, modificationReason: event.modification_reason, marketContext: event.market_context, createdDateTime: event.created_at, vtnComment: event.dis, eventStatus: calculateEventStatus( event.active_period.start_date, event.active_period.duration_seconds, event.active_period.notification_duration_seconds, event.active_period.ramp_up_duration_seconds, event.cancelled, ), testEvent: event.test_event, priority: event.priority, }, activePeriod: { startDate: event.active_period.start_date, duration: calculateOadrDuration(event.active_period.duration_seconds), notificationDuration: calculateOadrDuration( event.active_period.notification_duration_seconds, ), toleranceTolerateStartAfter: calculateOadrDuration( event.active_period.start_tolerance_duration_seconds, ), rampUpDuration: calculateOadrDuration( event.active_period.ramp_up_duration_seconds, ), recoveryDuration: calculateOadrDuration( event.active_period.recovery_duration_seconds, ), }, signals: { event: calculateEventSignals(event.signals.event), baseline: calculateBaselineSignal(event.signals.baseline), }, targets: calculateTargets(event.targets), responseRequired: event.response_required ? 'always' : 'never', }; } async function retrieveEvents( oadrRequestEvent, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'retrieveEvents', oadrRequestEvent, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = oadrRequestEvent.venId; if (!requestVenId) { const error = new Error('No VenID in request'); error.responseCode = 452; throw error; } if (requestVenId !== clientCertificateFingerprint) { // as per certification item #512, venId MUST be case-sensitive const error = new Error('VenID does not match certificate'); error.responseCode = 452; throw error; } if (!clientCertificateCn) { const error = new Error('Could not determine CN from client certificate'); error.responseCode = 452; throw error; } const ven = await nantum.getVenRegistration(clientCertificateFingerprint); if (!ven) { const error = new Error('VEN is not registered'); error.responseCode = 452; throw error; } const events = await getOadrEvents(ven, false); return { _type: 'oadrDistributeEvent', responseCode: '200', responseDescription: 'OK', responseRequestId: oadrRequestEvent.requestId || '', requestId: oadrRequestEvent.requestId || '', vtnId: vtnId, events, }; } /* qualifiedEvent is the combination of eventId & modificationNumber */ function eventResponseMatchesValidEvent(eventResponse, oadrEvents) { return ( oadrEvents.filter(oadrEvent => { return ( oadrEvent.eventDescriptor.eventId === eventResponse.eventId && oadrEvent.eventDescriptor.modificationNumber === eventResponse.modificationNumber && oadrEvent.eventDescriptor.status !== 'cancelled' && oadrEvent.eventDescriptor.status !== 'completed' ); }).length > 0 ); } async function validateEventResponses(venRegistration, eventResponses) { const events = await nantum.getEvents(venRegistration._id); const oadrEvents = events.map(event => convertToOadrEvent(event)); const staleResponses = eventResponses.filter( eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents), ); if (staleResponses.length > 0) { const error = new Error('Event response references invalid event'); error.responseCode = '454'; throw error; } } async function updateOptType( oadrCreatedEvent, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'updateOptType', oadrCreatedEvent, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = oadrCreatedEvent.venId; validateVenId(requestVenId, clientCertificateFingerprint, true); const venRegistration = await nantum.getVenRegistration(clientCertificateFingerprint); if (!venRegistration) { const error = new Error('VEN is not registered'); error.responseCode = 452; throw error; } const ven = await nantum.getVen(venRegistration._id); if (!ven) { const error = new Error('VEN registration is not linked to VEN'); error.responseCode = 452; throw error; } try { await validateEventResponses(venRegistration, oadrCreatedEvent.eventResponses); for (const eventResponse of oadrCreatedEvent.eventResponses) { const existingResponse = await nantum.getEventResponse( ven._id, eventResponse.eventId, eventResponse.modificationNumber, ); if (existingResponse != null) { await nantum.updateEventResponse(existingResponse._id, { opt_type: eventResponse.optType, }); } else { await nantum.createEventResponse({ oadr_event_id: eventResponse.eventId, oadr_ven_id: ven._id, modification_number: eventResponse.modificationNumber, opt_type: eventResponse.optType, }); } } return { _type: 'oadrResponse', responseCode: '200', responseDescription: 'OK', venId: clientCertificateFingerprint, }; } catch (e) { return { _type: 'oadrResponse', responseCode: e.responseCode || '454', responseDescription: e.message || 'Invalid event response received', venId: clientCertificateFingerprint, }; } } function eventHasBeenSeenByVen(seenEvents, event) { return ( seenEvents.filter( seenEvent => seenEvent.oadr_event_id === event.eventDescriptor.eventId && seenEvent.modification_number === event.eventDescriptor.modificationNumber, ).length > 0 ); } function eventIsVisible(event) { return event.status !== 'completed' && event.status !== 'none'; } async function pruneEvents(venRegistrationId, events) { const seenEvents = await nantum.getSeenEvents(venRegistrationId); return events.filter( event => !eventHasBeenSeenByVen(seenEvents, event) && eventIsVisible(event), ); } async function markEventsAsSeen(venRegistration, events) { for (const event of events) { await nantum.markEventAsSeen( venRegistration._id, event.eventDescriptor.eventId, event.eventDescriptor.modificationNumber, ); } } async function getOadrEvents(venRegistration, pruneSeen) { const events = await nantum.getEvents(venRegistration._id); const oadrEvents = events.map(event => convertToOadrEvent(event)); return pruneSeen ? pruneEvents(venRegistration._id,oadrEvents) : oadrEvents; } async function pollForEvents( oadrPoll, clientCertificateCn, clientCertificateFingerprint, ) { logger.info( 'pollForEvents', oadrPoll, clientCertificateCn, clientCertificateFingerprint, ); const requestVenId = oadrPoll.venId; validateVenId(requestVenId, clientCertificateFingerprint, true); const ven = await nantum.getVenRegistration(clientCertificateFingerprint); if (ven == null) { throw new Error(`Ven ${clientCertificateFingerprint} must be registered`); } const events = await getOadrEvents(ven, true); await markEventsAsSeen(ven, events); if (events.length > 0) { return { _type: 'oadrDistributeEvent', responseCode: '200', responseDescription: 'OK', responseRequestId: '', // required field, but empty is allowed as per spec requestId: '', vtnId: vtnId, events, }; } return undefined; } function validateVenId(requestVenId, clientCertificateFingerprint, required) { if (requestVenId === clientCertificateFingerprint) { return; } if (!required && requestVenId == null) { return; } if (required && requestVenId == null) { const error = new Error('VenID is missing'); error.responseCode = 452; throw error; } const error = new Error('VenID is invalid'); error.responseCode = 452; throw error; } module.exports = { pollForEvents, retrieveEvents, updateOptType, };