|
|
@@ -4,122 +4,142 @@ const logger = require('../logger');
|
|
|
const nantum = require('../modules/nantum');
|
|
|
const { vtnId } = require('../config');
|
|
|
|
|
|
-function calculateEventStatus(notificationTime, startTime, endTime) {
|
|
|
+function calculateEventStatus(
|
|
|
+ startDate,
|
|
|
+ durationSeconds,
|
|
|
+ notificationDurationSeconds,
|
|
|
+ rampUpDurationSeconds,
|
|
|
+ cancelled,
|
|
|
+) {
|
|
|
+ if (cancelled) return 'cancelled';
|
|
|
const nowMillis = new Date().getTime();
|
|
|
- if (nowMillis < new Date(startTime).getTime()) {
|
|
|
- return 'far';
|
|
|
+ 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 > new Date(endTime).getTime()) {
|
|
|
- return 'completed';
|
|
|
+
|
|
|
+ if (nowMillis < startMillis) {
|
|
|
+ if (nowMillis < rampStartMillis) {
|
|
|
+ return 'far';
|
|
|
+ }
|
|
|
+ return 'near';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (nowMillis < endMillis) {
|
|
|
+ return 'active';
|
|
|
}
|
|
|
|
|
|
- return 'active';
|
|
|
+ return 'completed';
|
|
|
}
|
|
|
|
|
|
-function calculateDurationSeconds(startTime, endTime) {
|
|
|
- return Math.round(
|
|
|
- (new Date(endTime).getTime() - new Date(startTime).getTime()) / 1000,
|
|
|
- );
|
|
|
+function calculateOadrDuration(seconds) {
|
|
|
+ if (seconds == null) return;
|
|
|
+ return `PT${seconds}S`;
|
|
|
}
|
|
|
|
|
|
-function calculateDuration(startTime, endTime) {
|
|
|
- return `PT${calculateDurationSeconds(startTime, endTime)}S`;
|
|
|
+function calculateEventIntervals(intervals) {
|
|
|
+ return intervals.map(interval => {
|
|
|
+ return {
|
|
|
+ signalPayloads: interval.signal_payloads,
|
|
|
+ duration: calculateOadrDuration(interval.duration_seconds),
|
|
|
+ uid: interval.uid,
|
|
|
+ };
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
-function calculateNotificationDuration(notificationTime, startTime) {
|
|
|
- if (!notificationTime) {
|
|
|
- return 'PT0S';
|
|
|
- }
|
|
|
- return calculateDuration(notificationTime, startTime);
|
|
|
+function calculateItemBase(itemBase) {
|
|
|
+ return {
|
|
|
+ type: itemBase.type,
|
|
|
+ description: itemBase.dis,
|
|
|
+ units: itemBase.units,
|
|
|
+ siScaleCode: itemBase.si_scale_code,
|
|
|
+ powerAttributes: itemBase.power_attributes,
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
-function calculateEventIntervals(eventInfoValues, eventDurationSeconds) {
|
|
|
- //TODO: this is likely incorrect. Get more details on the event_info_value data model.
|
|
|
-
|
|
|
- let result = [];
|
|
|
-
|
|
|
- for (let i = 0; i < eventInfoValues.length; i++) {
|
|
|
- const eventInfoValue = eventInfoValues[i];
|
|
|
- const nextOffset =
|
|
|
- i === eventInfoValues.length - 1
|
|
|
- ? eventDurationSeconds - eventInfoValue.timeOffset
|
|
|
- : eventInfoValues[i + 1].timeOffset;
|
|
|
- result.push({
|
|
|
- signalPayloads: [eventInfoValue.value],
|
|
|
- duration: `PT${nextOffset}S`,
|
|
|
- uid: `${i + 1}`,
|
|
|
- });
|
|
|
- }
|
|
|
- return result;
|
|
|
+function calculateTargets(targets) {
|
|
|
+ return targets.map(target => {
|
|
|
+ return {
|
|
|
+ type: target.target_type,
|
|
|
+ value: target.value,
|
|
|
+ };
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
-function calculateEventSignals(eventInstances, eventDurationSeconds) {
|
|
|
- return eventInstances.map(eventInstance => {
|
|
|
+function calculateEventSignals(signals) {
|
|
|
+ return signals.map(signal => {
|
|
|
return {
|
|
|
- signalName: eventInstance.event_type_id,
|
|
|
- signalId: '112233445566',
|
|
|
- signalType: 'level',
|
|
|
- intervals: calculateEventIntervals(
|
|
|
- eventInstance.event_info_values,
|
|
|
- eventDurationSeconds,
|
|
|
- ),
|
|
|
+ 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 convertToOadrEvents(nantumEvent) {
|
|
|
- if (!nantumEvent.dr_event_data) {
|
|
|
- // no event
|
|
|
- return [];
|
|
|
- }
|
|
|
- const nowMillis = new Date().getTime();
|
|
|
- if (
|
|
|
- nowMillis < new Date(nantumEvent.dr_event_data.notification_time).getTime()
|
|
|
- ) {
|
|
|
- return []; // not in the notification period yet
|
|
|
- }
|
|
|
+function calculateBaselineSignal(nantumBaseline) {
|
|
|
+ return {
|
|
|
+ baselineName: nantumBaseline.baseline_name,
|
|
|
+ baselineId: nantumBaseline.baseline_id,
|
|
|
+ duration: calculateOadrDuration(nantumBaseline.duration_seconds),
|
|
|
+ startDate: nantumBaseline.start_date,
|
|
|
+ intervals: calculateEventIntervals(nantumBaseline.intervals),
|
|
|
+ };
|
|
|
+}
|
|
|
|
|
|
- return [
|
|
|
- {
|
|
|
- eventDescriptor: {
|
|
|
- eventId: nantumEvent.event_identifier,
|
|
|
- modificationNumber: nantumEvent.event_mod_number,
|
|
|
- marketContext: 'http://MarketContext1',
|
|
|
- createdDateTime: '2020-04-14T16:06:39.000Z',
|
|
|
- eventStatus: calculateEventStatus(
|
|
|
- nantumEvent.dr_event_data.notification_time,
|
|
|
- nantumEvent.dr_event_data.start_time,
|
|
|
- nantumEvent.dr_event_data.end_time,
|
|
|
- ),
|
|
|
- testEvent: nantumEvent.test_event,
|
|
|
- priority: 0,
|
|
|
- },
|
|
|
- activePeriod: {
|
|
|
- startDate: nantumEvent.dr_event_data.start_time,
|
|
|
- duration: calculateDuration(
|
|
|
- nantumEvent.dr_event_data.start_time,
|
|
|
- nantumEvent.dr_event_data.end_time,
|
|
|
- ),
|
|
|
- notificationDuration: calculateNotificationDuration(
|
|
|
- nantumEvent.dr_event_data.notification_time,
|
|
|
- nantumEvent.dr_event_data.start_time,
|
|
|
- ),
|
|
|
- },
|
|
|
- signals: {
|
|
|
- event: calculateEventSignals(
|
|
|
- nantumEvent.dr_event_data.event_instance,
|
|
|
- calculateDurationSeconds(
|
|
|
- nantumEvent.dr_event_data.start_time,
|
|
|
- nantumEvent.dr_event_data.end_time,
|
|
|
- ),
|
|
|
- ),
|
|
|
- },
|
|
|
- target: {
|
|
|
- venId: [nantumEvent.client_id],
|
|
|
- },
|
|
|
- responseRequired: 'always',
|
|
|
+function convertToOadrEvent(ven, 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(
|
|
|
@@ -155,15 +175,22 @@ async function retrieveEvents(
|
|
|
throw error;
|
|
|
}
|
|
|
|
|
|
- const event = await nantum.fetchEvent(requestVenId);
|
|
|
+ const ven = await nantum.getVen(clientCertificateFingerprint);
|
|
|
+ if (!ven) {
|
|
|
+ const error = new Error('VEN is not registered');
|
|
|
+ error.responseCode = 452;
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ const events = await getPrunedOadrEvents(ven);
|
|
|
|
|
|
return {
|
|
|
+ _type: 'oadrDistributeEvent',
|
|
|
responseCode: '200',
|
|
|
responseDescription: 'OK',
|
|
|
responseRequestId: oadrRequestEvent.requestId || '',
|
|
|
requestId: oadrRequestEvent.requestId || '',
|
|
|
vtnId: vtnId,
|
|
|
- events: convertToOadrEvents(event),
|
|
|
+ events,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -182,9 +209,9 @@ function eventResponseMatchesValidEvent(eventResponse, oadrEvents) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-async function validateEventResponses(venId, eventResponses) {
|
|
|
- const event = await nantum.fetchEvent(venId);
|
|
|
- const oadrEvents = convertToOadrEvents(event);
|
|
|
+async function validateEventResponses(ven, eventResponses) {
|
|
|
+ const events = await nantum.getEvents();
|
|
|
+ const oadrEvents = events.map(event => convertToOadrEvent(events, event));
|
|
|
const staleResponses = eventResponses.filter(
|
|
|
eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents),
|
|
|
);
|
|
|
@@ -211,24 +238,29 @@ async function updateOptType(
|
|
|
const requestVenId = oadrCreatedEvent.venId;
|
|
|
validateVenId(requestVenId, clientCertificateFingerprint, true);
|
|
|
|
|
|
- let opted = await nantum.fetchOpted(requestVenId);
|
|
|
+ const ven = await nantum.getVen(clientCertificateFingerprint);
|
|
|
|
|
|
try {
|
|
|
- await validateEventResponses(requestVenId, oadrCreatedEvent.eventResponses);
|
|
|
+ await validateEventResponses(ven, oadrCreatedEvent.eventResponses);
|
|
|
for (const eventResponse of oadrCreatedEvent.eventResponses) {
|
|
|
- // remove existing opts for this eventId
|
|
|
- opted = [
|
|
|
- ...opted.filter(
|
|
|
- optedItem => optedItem.eventId !== eventResponse.eventId,
|
|
|
- ),
|
|
|
- ];
|
|
|
- opted.push({
|
|
|
- eventId: eventResponse.eventId,
|
|
|
- modificationNumber: eventResponse.modificationNumber,
|
|
|
- optType: eventResponse.optType,
|
|
|
- });
|
|
|
+ const existingResponse = await nantum.getEventResponse(
|
|
|
+ ven,
|
|
|
+ eventResponse.eventId,
|
|
|
+ eventResponse.modificationNumber,
|
|
|
+ );
|
|
|
+ if (existingResponse != null) {
|
|
|
+ await nantum.updateEventResponse(existingResponse._id, {
|
|
|
+ opt_type: eventResponse.optType,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ await nantum.createEventResponse({
|
|
|
+ event_id: eventResponse.eventId,
|
|
|
+ ven_id: ven._id,
|
|
|
+ modification_number: eventResponse.modificationNumber,
|
|
|
+ opt_type: eventResponse.optType,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
- await nantum.updateOpted(requestVenId, opted);
|
|
|
|
|
|
return {
|
|
|
_type: 'oadrResponse',
|
|
|
@@ -246,16 +278,42 @@ async function updateOptType(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function filterOutAcknowledgedEvents(venId, events) {
|
|
|
- const opted = (await nantum.fetchOpted(venId)) || [];
|
|
|
+function eventHasBeenSeenByVen(ven, event) {
|
|
|
+ return (
|
|
|
+ (ven.seen_events || []).filter(
|
|
|
+ seenEvent =>
|
|
|
+ seenEvent.event_id === event.eventDescriptor.eventId &&
|
|
|
+ seenEvent.modification_number ===
|
|
|
+ event.eventDescriptor.modificationNumber,
|
|
|
+ ).length > 0
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function eventIsVisible(event) {
|
|
|
+ return event.status !== 'completed' && event.status !== 'none';
|
|
|
+}
|
|
|
+
|
|
|
+function pruneEvents(ven, events) {
|
|
|
return events.filter(
|
|
|
- event =>
|
|
|
- opted.filter(
|
|
|
- optedItem =>
|
|
|
- optedItem.eventId === event.eventDescriptor.eventId &&
|
|
|
- optedItem.modificationNumber ===
|
|
|
- event.eventDescriptor.modificationNumber,
|
|
|
- ).length === 0,
|
|
|
+ event => !eventHasBeenSeenByVen(ven, event) && eventIsVisible(event),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+async function markEventsAsSeen(ven, events) {
|
|
|
+ for (const event of events) {
|
|
|
+ await nantum.markEventAsSeen(
|
|
|
+ ven,
|
|
|
+ event.eventDescriptor.eventId,
|
|
|
+ event.eventDescriptor.modificationNumber,
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function getPrunedOadrEvents(ven) {
|
|
|
+ const events = await nantum.getEvents();
|
|
|
+ return pruneEvents(
|
|
|
+ ven,
|
|
|
+ events.map(event => convertToOadrEvent(ven, event)),
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -273,14 +331,16 @@ async function pollForEvents(
|
|
|
|
|
|
const requestVenId = oadrPoll.venId;
|
|
|
validateVenId(requestVenId, clientCertificateFingerprint, true);
|
|
|
+ const ven = await nantum.getVen(clientCertificateFingerprint);
|
|
|
+ if (ven == null) {
|
|
|
+ throw new Error(`Ven ${clientCertificateFingerprint} must be registered`);
|
|
|
+ }
|
|
|
|
|
|
- const event = await nantum.fetchEvent(requestVenId);
|
|
|
- const filteredEvents = await filterOutAcknowledgedEvents(
|
|
|
- requestVenId,
|
|
|
- convertToOadrEvents(event),
|
|
|
- );
|
|
|
+ const events = await getPrunedOadrEvents(ven);
|
|
|
+
|
|
|
+ await markEventsAsSeen(ven, events);
|
|
|
|
|
|
- if (filteredEvents.length > 0) {
|
|
|
+ if (events.length > 0) {
|
|
|
return {
|
|
|
_type: 'oadrDistributeEvent',
|
|
|
responseCode: '200',
|
|
|
@@ -288,7 +348,7 @@ async function pollForEvents(
|
|
|
responseRequestId: '', // required field, but empty is allowed as per spec
|
|
|
requestId: '',
|
|
|
vtnId: vtnId,
|
|
|
- events: filteredEvents,
|
|
|
+ events,
|
|
|
};
|
|
|
}
|
|
|
return undefined;
|