event.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. 'use strict';
  2. const logger = require('../logger');
  3. const nantum = require('../modules/nantum');
  4. const { vtnId } = require('../config');
  5. function calculateEventStatus(
  6. startDate,
  7. durationSeconds,
  8. notificationDurationSeconds,
  9. rampUpDurationSeconds,
  10. cancelled,
  11. ) {
  12. if (cancelled) return 'cancelled';
  13. const nowMillis = new Date().getTime();
  14. const startMillis = new Date(startDate).getTime();
  15. const endMillis = startMillis + durationSeconds * 1000;
  16. const notificationStartMillis =
  17. startMillis - (notificationDurationSeconds || 0) * 1000;
  18. const rampStartMillis = startMillis - (rampUpDurationSeconds || 0) * 1000;
  19. if (nowMillis < notificationStartMillis) {
  20. return 'none';
  21. }
  22. if (nowMillis < startMillis) {
  23. if (nowMillis < rampStartMillis) {
  24. return 'far';
  25. }
  26. return 'near';
  27. }
  28. if (nowMillis < endMillis) {
  29. return 'active';
  30. }
  31. return 'completed';
  32. }
  33. function calculateOadrDuration(seconds) {
  34. if (seconds == null) return;
  35. return `PT${seconds}S`;
  36. }
  37. function calculateEventIntervals(intervals) {
  38. return intervals.map(interval => {
  39. return {
  40. signalPayloads: interval.signal_payloads,
  41. duration: calculateOadrDuration(interval.duration_seconds),
  42. uid: interval.uid,
  43. };
  44. });
  45. }
  46. function calculateItemBase(itemBase) {
  47. return {
  48. type: itemBase.type,
  49. description: itemBase.dis,
  50. units: itemBase.units,
  51. siScaleCode: itemBase.si_scale_code,
  52. powerAttributes: itemBase.power_attributes,
  53. };
  54. }
  55. function calculateTargets(targets) {
  56. return targets.map(target => {
  57. return {
  58. type: target.target_type,
  59. value: target.value,
  60. };
  61. });
  62. }
  63. function calculateEventSignals(signals) {
  64. return signals.map(signal => {
  65. return {
  66. signalName: signal.signal_name,
  67. signalId: signal.signal_id,
  68. signalType: signal.signal_type,
  69. currentValue: signal.current_value,
  70. duration: calculateOadrDuration(signal.duration_seconds),
  71. startDate: signal.start_date,
  72. intervals: calculateEventIntervals(signal.intervals),
  73. itemBase: calculateItemBase(signal.item_base),
  74. };
  75. });
  76. }
  77. function calculateBaselineSignal(nantumBaseline) {
  78. return {
  79. baselineName: nantumBaseline.baseline_name,
  80. baselineId: nantumBaseline.baseline_id,
  81. duration: calculateOadrDuration(nantumBaseline.duration_seconds),
  82. startDate: nantumBaseline.start_date,
  83. intervals: calculateEventIntervals(nantumBaseline.intervals),
  84. };
  85. }
  86. function convertToOadrEvent(ven, event) {
  87. return {
  88. eventDescriptor: {
  89. eventId: event._id,
  90. modificationNumber: event.modification_number,
  91. modificationDateTime: event.modification_date,
  92. modificationReason: event.modification_reason,
  93. marketContext: event.market_context,
  94. createdDateTime: event.created_at,
  95. vtnComment: event.dis,
  96. eventStatus: calculateEventStatus(
  97. event.active_period.start_date,
  98. event.active_period.duration_seconds,
  99. event.active_period.notification_duration_seconds,
  100. event.active_period.ramp_up_duration_seconds,
  101. event.cancelled,
  102. ),
  103. testEvent: event.test_event,
  104. priority: event.priority,
  105. },
  106. activePeriod: {
  107. startDate: event.active_period.start_date,
  108. duration: calculateOadrDuration(event.active_period.duration_seconds),
  109. notificationDuration: calculateOadrDuration(
  110. event.active_period.notification_duration_seconds,
  111. ),
  112. toleranceTolerateStartAfter: calculateOadrDuration(
  113. event.active_period.start_tolerance_duration_seconds,
  114. ),
  115. rampUpDuration: calculateOadrDuration(
  116. event.active_period.ramp_up_duration_seconds,
  117. ),
  118. recoveryDuration: calculateOadrDuration(
  119. event.active_period.recovery_duration_seconds,
  120. ),
  121. },
  122. signals: {
  123. event: calculateEventSignals(event.signals.event),
  124. baseline: calculateBaselineSignal(event.signals.baseline),
  125. },
  126. targets: calculateTargets(event.targets),
  127. responseRequired: event.response_required ? 'always' : 'never',
  128. };
  129. }
  130. async function retrieveEvents(
  131. oadrRequestEvent,
  132. clientCertificateCn,
  133. clientCertificateFingerprint,
  134. ) {
  135. logger.info(
  136. 'retrieveEvents',
  137. oadrRequestEvent,
  138. clientCertificateCn,
  139. clientCertificateFingerprint,
  140. );
  141. const requestVenId = oadrRequestEvent.venId;
  142. if (!requestVenId) {
  143. const error = new Error('No VenID in request');
  144. error.responseCode = 452;
  145. throw error;
  146. }
  147. if (requestVenId !== clientCertificateFingerprint) {
  148. // as per certification item #512, venId MUST be case-sensitive
  149. const error = new Error('VenID does not match certificate');
  150. error.responseCode = 452;
  151. throw error;
  152. }
  153. if (!clientCertificateCn) {
  154. const error = new Error('Could not determine CN from client certificate');
  155. error.responseCode = 452;
  156. throw error;
  157. }
  158. const ven = await nantum.getVen(clientCertificateFingerprint);
  159. if (!ven) {
  160. const error = new Error('VEN is not registered');
  161. error.responseCode = 452;
  162. throw error;
  163. }
  164. const events = await getPrunedOadrEvents(ven);
  165. return {
  166. _type: 'oadrDistributeEvent',
  167. responseCode: '200',
  168. responseDescription: 'OK',
  169. responseRequestId: oadrRequestEvent.requestId || '',
  170. requestId: oadrRequestEvent.requestId || '',
  171. vtnId: vtnId,
  172. events,
  173. };
  174. }
  175. /* qualifiedEvent is the combination of eventId & modificationNumber */
  176. function eventResponseMatchesValidEvent(eventResponse, oadrEvents) {
  177. return (
  178. oadrEvents.filter(oadrEvent => {
  179. return (
  180. oadrEvent.eventDescriptor.eventId === eventResponse.eventId &&
  181. oadrEvent.eventDescriptor.modificationNumber ===
  182. eventResponse.modificationNumber &&
  183. oadrEvent.eventDescriptor.status !== 'cancelled' &&
  184. oadrEvent.eventDescriptor.status !== 'completed'
  185. );
  186. }).length > 0
  187. );
  188. }
  189. async function validateEventResponses(ven, eventResponses) {
  190. const events = await nantum.getEvents();
  191. const oadrEvents = events.map(event => convertToOadrEvent(events, event));
  192. const staleResponses = eventResponses.filter(
  193. eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents),
  194. );
  195. if (staleResponses.length > 0) {
  196. const error = new Error('Event response references invalid event');
  197. error.responseCode = '454';
  198. throw error;
  199. }
  200. }
  201. async function updateOptType(
  202. oadrCreatedEvent,
  203. clientCertificateCn,
  204. clientCertificateFingerprint,
  205. ) {
  206. logger.info(
  207. 'updateOptType',
  208. oadrCreatedEvent,
  209. clientCertificateCn,
  210. clientCertificateFingerprint,
  211. );
  212. const requestVenId = oadrCreatedEvent.venId;
  213. validateVenId(requestVenId, clientCertificateFingerprint, true);
  214. const ven = await nantum.getVen(clientCertificateFingerprint);
  215. try {
  216. await validateEventResponses(ven, oadrCreatedEvent.eventResponses);
  217. for (const eventResponse of oadrCreatedEvent.eventResponses) {
  218. const existingResponse = await nantum.getEventResponse(
  219. ven,
  220. eventResponse.eventId,
  221. eventResponse.modificationNumber,
  222. );
  223. if (existingResponse != null) {
  224. await nantum.updateEventResponse(existingResponse._id, {
  225. opt_type: eventResponse.optType,
  226. });
  227. } else {
  228. await nantum.createEventResponse({
  229. event_id: eventResponse.eventId,
  230. ven_id: ven._id,
  231. modification_number: eventResponse.modificationNumber,
  232. opt_type: eventResponse.optType,
  233. });
  234. }
  235. }
  236. return {
  237. _type: 'oadrResponse',
  238. responseCode: '200',
  239. responseDescription: 'OK',
  240. venId: clientCertificateFingerprint,
  241. };
  242. } catch (e) {
  243. return {
  244. _type: 'oadrResponse',
  245. responseCode: e.responseCode || '454',
  246. responseDescription: e.message || 'Invalid event response received',
  247. venId: clientCertificateFingerprint,
  248. };
  249. }
  250. }
  251. function eventHasBeenSeenByVen(ven, event) {
  252. return (
  253. (ven.seen_events || []).filter(
  254. seenEvent =>
  255. seenEvent.event_id === event.eventDescriptor.eventId &&
  256. seenEvent.modification_number ===
  257. event.eventDescriptor.modificationNumber,
  258. ).length > 0
  259. );
  260. }
  261. function eventIsVisible(event) {
  262. return event.status !== 'completed' && event.status !== 'none';
  263. }
  264. function pruneEvents(ven, events) {
  265. return events.filter(
  266. event => !eventHasBeenSeenByVen(ven, event) && eventIsVisible(event),
  267. );
  268. }
  269. async function markEventsAsSeen(ven, events) {
  270. for (const event of events) {
  271. await nantum.markEventAsSeen(
  272. ven,
  273. event.eventDescriptor.eventId,
  274. event.eventDescriptor.modificationNumber,
  275. );
  276. }
  277. }
  278. async function getPrunedOadrEvents(ven) {
  279. const events = await nantum.getEvents();
  280. return pruneEvents(
  281. ven,
  282. events.map(event => convertToOadrEvent(ven, event)),
  283. );
  284. }
  285. async function pollForEvents(
  286. oadrPoll,
  287. clientCertificateCn,
  288. clientCertificateFingerprint,
  289. ) {
  290. logger.info(
  291. 'pollForEvents',
  292. oadrPoll,
  293. clientCertificateCn,
  294. clientCertificateFingerprint,
  295. );
  296. const requestVenId = oadrPoll.venId;
  297. validateVenId(requestVenId, clientCertificateFingerprint, true);
  298. const ven = await nantum.getVen(clientCertificateFingerprint);
  299. if (ven == null) {
  300. throw new Error(`Ven ${clientCertificateFingerprint} must be registered`);
  301. }
  302. const events = await getPrunedOadrEvents(ven);
  303. await markEventsAsSeen(ven, events);
  304. if (events.length > 0) {
  305. return {
  306. _type: 'oadrDistributeEvent',
  307. responseCode: '200',
  308. responseDescription: 'OK',
  309. responseRequestId: '', // required field, but empty is allowed as per spec
  310. requestId: '',
  311. vtnId: vtnId,
  312. events,
  313. };
  314. }
  315. return undefined;
  316. }
  317. function validateVenId(requestVenId, clientCertificateFingerprint, required) {
  318. if (requestVenId === clientCertificateFingerprint) {
  319. return;
  320. }
  321. if (!required && requestVenId == null) {
  322. return;
  323. }
  324. if (required && requestVenId == null) {
  325. const error = new Error('VenID is missing');
  326. error.responseCode = 452;
  327. throw error;
  328. }
  329. const error = new Error('VenID is invalid');
  330. error.responseCode = 452;
  331. throw error;
  332. }
  333. module.exports = {
  334. pollForEvents,
  335. retrieveEvents,
  336. updateOptType,
  337. };