فهرست منبع

PROD-2707: Migrate from Postgres database to Nantum API

 * Generate OpenADR events using Nantum API `oadr_events`
 * Update optIn/optOut status using Nantum API `oadr_event_responses`
 * Synchronize VEN configuration using Nantum API `oadr_vens`
Blake Schneider 5 سال پیش
والد
کامیت
ac15f70ba6
51فایلهای تغییر یافته به همراه1817 افزوده شده و 1160 حذف شده
  1. 4 17
      README.md
  2. 81 86
      __tests__/integration/end-to-end.spec.js
  3. 0 45
      __tests__/unit/db/ven.spec.js
  4. 138 62
      __tests__/unit/modules/nantum-responses.js
  5. 11 42
      __tests__/unit/processes/event.spec.js
  6. 29 20
      __tests__/unit/processes/registration.spec.js
  7. 11 9
      __tests__/unit/processes/report.spec.js
  8. 81 0
      __tests__/unit/utils/fake-nantum-module.js
  9. 1 1
      __tests__/unit/xml/event/created-event.spec.js
  10. 8 0
      __tests__/unit/xml/event/distribute-event.spec.js
  11. 5 5
      __tests__/unit/xml/event/js-requests.js
  12. 173 102
      __tests__/unit/xml/event/js-responses.js
  13. 5 5
      __tests__/unit/xml/event/xml-requests.js
  14. 1 1
      __tests__/unit/xml/report/create-report.spec.js
  15. 1 1
      __tests__/unit/xml/report/created-report.spec.js
  16. 2 2
      __tests__/unit/xml/report/js-requests.js
  17. 4 4
      __tests__/unit/xml/report/js-responses.js
  18. 1 1
      __tests__/unit/xml/report/register-report.spec.js
  19. 1 1
      __tests__/unit/xml/report/registered-report.spec.js
  20. 3 3
      __tests__/unit/xml/report/xml-requests.js
  21. 3 3
      __tests__/unit/xml/report/xml-responses.js
  22. 3 4
      config/development.js
  23. 2 3
      config/production.js
  24. 2 3
      config/testing.js
  25. 0 15
      db/_db.js
  26. 0 10
      db/index.js
  27. 0 7
      db/models/index.js
  28. 0 18
      db/models/ven.js
  29. 0 16
      docker-compose.yml
  30. 0 3
      docker_run_psql.sh
  31. 1 1
      docker_run_tests.sh
  32. 0 2
      index.js
  33. 79 92
      modules/nantum.js
  34. 717 241
      package-lock.json
  35. 7 7
      package.json
  36. 190 130
      processes/event.js
  37. 41 41
      processes/registration.js
  38. 115 66
      processes/report.js
  39. 2 2
      xml/event/created-event.js
  40. 56 50
      xml/event/distribute-event.js
  41. 2 2
      xml/event/request-event.js
  42. 2 2
      xml/poll/oadr-response.js
  43. 2 2
      xml/register-party/cancel-party-registration.js
  44. 4 4
      xml/register-party/canceled-party-registration.js
  45. 10 10
      xml/register-party/create-party-registration.js
  46. 6 6
      xml/register-party/created-party-registration.js
  47. 3 3
      xml/report/created-report.js
  48. 2 2
      xml/report/register-report.js
  49. 2 2
      xml/report/registered-report.js
  50. 2 2
      xml/report/updated-report.js
  51. 4 4
      xml/shared.js

+ 4 - 17
README.md

@@ -9,9 +9,10 @@ RSA private key for Kinesis must be installed at `pem/private-key.pem`.
 
 ## Environment
 Please set the following environment variables:
+* `NANTUM_URL`: URL of Nantum API endpoint to use
+* `CLIENT_ID` / `CLIENT_SECRET`: Credentials to access Nantum API
 * `COMPANY`: Which company we're associated to
 * `NODE_ENV`: Which environment we're running in. Can be `production` | `development` | `test`.
-* `DB_URL`: The database URL used to store buffered sensor readings
 * `ENCRYPT_PASS`: The password used to encrypt the RSA private key, as well as `LOGGER_PEM`
 * `PORT`: The TCP port the webserver should bind to
 * `REGION`: AWS region to use for Kinesis
@@ -22,8 +23,8 @@ Please set the following environment variables:
 ## Running locally for development
 
 ### Environment
-At a minimum you will want to set `NODE_ENV` to `development`, `NO_AWS` to `true`, and configure a `DB_URL` to point to
-a Postgres database.
+At a minimum you will want to set `NODE_ENV` to `development`, `NO_AWS` to `true`, and configure `NANTUM_URL`, 
+`CLIENT_ID`, `CLIENT_SECRET`, and `COMPANY` to point to a Nantum instance.
 
 ### Build
 Ensure you have a `.npmrc` file with an authToken for the `@hw` and `@be` private repos. If you get an error `E401` it's likely
@@ -82,20 +83,6 @@ docker-compose up -d
 
 You can tweak the environment variables in `docker-compose.yml`.
 
-### Administering database
-You can run
-```
-./docker_run_psql.sh
-```
-
-To get a `psql` session for the Docker Postgres database.
-
-## Running locally with a Docker database
-
-If you don't want to spin up a separate Postgres database, you can follow the steps in `Running in Docker for development`,
-un-comment the 2 `port` lines under `db` in `docker-compose.yml`, then use a `DB_URL` of `postgres://vtn:vtn@127.0.0.1:55432/vtn_test`
-in your local NodeJS environment. This will let you change code quickly without rebuilding a Docker image.
-
 ## Client certificate authentication
 
 OpenADR VENs connect using a client TLS certificate. In this Docker-compose configuration, nginx provides:

+ 81 - 86
__tests__/integration/end-to-end.spec.js

@@ -1,18 +1,69 @@
 'use strict';
 
-const { readFileSync } = require('fs');
-const path = require('path');
 const { expect } = require('chai');
 const sinon = require('sinon');
+const { pki } = require('node-forge');
+const {
+  calculatePartialFingerprintOfEscapedPemCertificate,
+} = require('../../modules/certificate');
 
 const { Ven } = require('../../client/ven');
 const app = require('../../server');
-const { sequelize, Ven: VenDb } = require('../../db');
 const { port } = require('../../config');
 
-describe('VEN to VTN interactions', function() {
-  const venId = '17:32:59:FD:0E:B5:99:31:27:9C';
+function getVenClient() {
+  const commonName = 'aabbccddeeff';
+  const keypair = pki.rsa.generateKeyPair({ bits: 1024, e: 0x10001 });
+  const cert = pki.createCertificate();
+  cert.publicKey = keypair.publicKey;
+  cert.serialNumber = '01';
+  cert.validity.notBefore = new Date('2000-01-01T00:00:00.000Z');
+  cert.validity.notAfter = new Date('2100-01-01T00:00:00.000Z');
+
+  const attrs = [
+    {
+      name: 'commonName',
+      value: commonName,
+    },
+    {
+      name: 'countryName',
+      value: 'US',
+    },
+    {
+      shortName: 'ST',
+      value: 'New York',
+    },
+    {
+      name: 'localityName',
+      value: 'New York',
+    },
+    {
+      name: 'organizationName',
+      value: 'Test',
+    },
+    {
+      shortName: 'OU',
+      value: 'Test',
+    },
+  ];
+  cert.setSubject(attrs);
+  cert.setIssuer(attrs);
+  cert.sign(keypair.privateKey);
+  const clientCrtPem = pki.certificateToPem(cert);
+  const fingerprint = calculatePartialFingerprintOfEscapedPemCertificate(
+    clientCrtPem,
+  );
+
+  return new Ven(
+    `http://127.0.0.1:${port}`,
+    clientCrtPem,
+    commonName,
+    fingerprint,
+    'ven.js1',
+  );
+}
 
+describe('VEN to VTN interactions', function() {
   describe('registration and event retrieval', async function() {
     let clock;
 
@@ -26,20 +77,8 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
-      const clientCrtPem = readFileSync(
-        path.join(__dirname, 'integration-client.crt'),
-        'utf-8',
-      );
-      ven = new Ven(
-        `http://127.0.0.1:${port}`,
-        clientCrtPem,
-        'aabbccddeeff',
-        venId,
-        'ven.js1',
-      );
+      ven = getVenClient();
     });
 
     it('should successfully return a vtnId from queryRegistration', async () => {
@@ -75,7 +114,7 @@ describe('VEN to VTN interactions', function() {
     after(async () => {
       await app.stop();
     });
-  });
+  }).timeout(5000);
 
   describe('poll', async function() {
     let ven;
@@ -90,20 +129,8 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
-      const clientCrtPem = readFileSync(
-        path.join(__dirname, 'integration-client.crt'),
-        'utf-8',
-      );
-      ven = new Ven(
-        `http://127.0.0.1:${port}`,
-        clientCrtPem,
-        'aabbccddeeff',
-        venId,
-        'ven.js1',
-      );
+      ven = getVenClient();
       await ven.register();
     });
 
@@ -113,10 +140,15 @@ describe('VEN to VTN interactions', function() {
       expect(pollResponse.events.length).to.eql(1);
     });
 
+    it('should not return the same event twice', async () => {
+      const pollResponse = await ven.poll();
+      expect(pollResponse._type).to.eql('oadrResponse');
+    });
+
     after(async () => {
       await app.stop();
     });
-  });
+  }).timeout(5000);
 
   describe('report', async function() {
     let ven;
@@ -131,27 +163,10 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
-      const clientCrtPem = readFileSync(
-        path.join(__dirname, 'integration-client.crt'),
-        'utf-8',
-      );
-      ven = new Ven(
-        `http://127.0.0.1:${port}`,
-        clientCrtPem,
-        'aabbccddeeff',
-        venId,
-        'ven.js1',
-      );
+      ven = getVenClient();
       await ven.register();
-
-      const pollResponse = await ven.poll();
-      const events = pollResponse.events;
-      const eventId = events[0].eventDescriptor.eventId;
-      const modificationNumber = events[0].eventDescriptor.modificationNumber;
-      await ven.opt('optIn', eventId, modificationNumber);
+      await ven.poll(); // poll for any events that may be waiting
     });
 
     it('should successfully subscribe to reports and receive data', async () => {
@@ -164,7 +179,7 @@ describe('VEN to VTN interactions', function() {
           reportName: 'METADATA_TELEMETRY_STATUS',
           descriptions: [
             {
-              reportId: 'ts1',
+              reportId: 'TelemetryStatusReport',
               reportType: 'x-resourceStatus',
               readingType: 'x-notApplicable',
               samplingRate: {
@@ -183,7 +198,7 @@ describe('VEN to VTN interactions', function() {
           reportName: 'METADATA_TELEMETRY_USAGE',
           descriptions: [
             {
-              reportId: 'rep1',
+              reportId: 'TelemetryUsageReport',
               reportType: 'usage',
               readingType: 'Direct Read',
               samplingRate: {
@@ -230,7 +245,7 @@ describe('VEN to VTN interactions', function() {
                 {
                   dataQuality: 'Quality Good - Non Specific',
                   payloadFloat: 161.97970171999845,
-                  reportId: 'rep1',
+                  reportId: 'TelemetryUsageReport',
                 },
               ],
               startDate: '2020-05-08T21:26:49.562-06:00',
@@ -260,7 +275,7 @@ describe('VEN to VTN interactions', function() {
                       },
                     },
                   },
-                  reportId: 'rep1',
+                  reportId: 'TelemetryStatusReport',
                 },
               ],
               startDate: '2020-05-13T10:56:11.058-06:00',
@@ -274,12 +289,12 @@ describe('VEN to VTN interactions', function() {
       ];
 
       await ven.sendReportData(reports);
-    });
+    }).timeout(10000);
 
     after(async () => {
       await app.stop();
     });
-  });
+  }).timeout(5000);
 
   describe('optIn', async function() {
     let clock;
@@ -294,47 +309,27 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
-      const clientCrtPem = readFileSync(
-        path.join(__dirname, 'integration-client.crt'),
-        'utf-8',
-      );
-      ven = new Ven(
-        `http://127.0.0.1:${port}`,
-        clientCrtPem,
-        'aabbccddeeff',
-        venId,
-        'ven.js1',
-      );
+      ven = getVenClient();
       await ven.register();
       pollResponse = await ven.poll();
       events = pollResponse.events;
     });
 
-    it('should successfully poll for events', async () => {
-      expect(pollResponse._type).to.eql('oadrDistributeEvent');
-      expect(pollResponse.events.length).to.eql(1);
-    });
-
-    it('should return same events if not opted', async () => {
-      const pollResponse = await ven.poll();
-      expect(pollResponse._type).to.eql('oadrDistributeEvent');
-      expect(pollResponse.events.length).to.eql(1);
-    });
-
-    it('should return no events if opted', async () => {
+    it('should successfully opt in', async () => {
       const eventId = events[0].eventDescriptor.eventId;
       const modificationNumber = events[0].eventDescriptor.modificationNumber;
       await ven.opt('optIn', eventId, modificationNumber);
+    });
 
-      const pollResponse = await ven.poll();
-      expect(pollResponse._type).to.eql('oadrResponse');
+    it('should be able to opt out after opting in', async () => {
+      const eventId = events[0].eventDescriptor.eventId;
+      const modificationNumber = events[0].eventDescriptor.modificationNumber;
+      await ven.opt('optOut', eventId, modificationNumber);
     });
 
     after(async () => {
       await app.stop();
     });
-  });
+  }).timeout(5000);
 });

+ 0 - 45
__tests__/unit/db/ven.spec.js

@@ -1,45 +0,0 @@
-'use strict';
-
-const { expect } = require('chai');
-const { sequelize, Ven } = require('../../../db');
-const { v4 } = require('uuid');
-
-describe('VEN Model', function() {
-  before(async () => {
-    await sequelize.sync();
-  });
-
-  describe('Retrieval', function() {
-    it('returns no results for unknown ven_id', async () => {
-      const randomVenId = v4();
-      const existing = await Ven.findOne({ where: { ven_id: randomVenId } });
-      expect(existing).to.be.null;
-    });
-
-    it('retrieves a saved record when queried by ven_id', async () => {
-      const randomVenId = v4();
-      const randomCommonName = v4();
-      const ven = new Ven();
-      ven.ven_id = randomVenId;
-      ven.common_name = randomCommonName;
-      await ven.save();
-      const existing = await Ven.findOne({ where: { ven_id: randomVenId } });
-      expect(existing).to.not.be.undefined;
-      expect(existing.common_name).to.eql(randomCommonName);
-    });
-
-    it('retrieves a saved record when queried by common_name', async () => {
-      const randomVenId = v4();
-      const randomCommonName = v4();
-      const ven = new Ven();
-      ven.ven_id = randomVenId;
-      ven.common_name = randomCommonName;
-      await ven.save();
-      const existing = await Ven.findOne({
-        where: { common_name: randomCommonName },
-      });
-      expect(existing).to.not.be.undefined;
-      expect(existing.common_name).to.eql(randomCommonName);
-    });
-  });
-});

+ 138 - 62
__tests__/unit/modules/nantum-responses.js

@@ -1,84 +1,160 @@
 'use strict';
 
 const sampleEvent1 = {
-  event_identifier: 'a2fa542eca8d4e829ff5c0f0c8e68710',
-  client_id: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
-  test_event: false,
-  event_mod_number: 2,
-  offLine: false,
-  dr_mode_data: {
-    operation_mode_value: 'NORMAL',
-    // event_status: 'NEAR',
-    // currentTime: 'xxxxx',
+  _id: '5f076b8e4d122ec1152361ec',
+  active_period: {
+    duration_seconds: 1800,
+    notification_duration_seconds: 86400,
+    ramp_up_duration_seconds: 3600,
+    start_tolerance_duration_seconds: 0,
+    start_date: '2020-07-10T00:00:00Z',
   },
-  dr_event_data: {
-    notification_time: '2020-04-25T22:50:00.000Z',
-    start_time: '2020-04-26T23:00:00.000Z',
-    end_time: '2020-04-26T23:55:00.000Z',
-    event_instance: [
+  cancelled: false,
+  company: 'cyberdyne',
+  created_at: '2020-07-09T19:10:06.332Z',
+  dis: 'Test Event 1',
+  market_context: 'http://emix',
+  modification_number: 0,
+  priority: 0,
+  response_required: true,
+  signals: {
+    event: [
       {
-        event_type_id: 'LOAD_AMOUNT',
-        event_info_values: [
-          { value: 41, timeOffset: 0 },
-          { value: 42, timeOffset: 10 },
+        signal_id: 'id1',
+        signal_name: 'BID_LOAD',
+        signal_type: 'setpoint',
+        current_value: 45.5,
+        duration_seconds: 1800,
+        start_date: '2020-07-10T00:00:00.000Z',
+        intervals: [
+          {
+            duration_seconds: 1740,
+            signal_payloads: [50],
+            uid: '0',
+          },
+          {
+            duration_seconds: 60,
+            signal_payloads: [51],
+            uid: '1',
+          },
         ],
+        item_base: {
+          type: 'power-real',
+          dis: 'RealPower',
+          units: 'W',
+          si_scale_code: 'none',
+          power_attributes: {
+            hertz: 60,
+            voltage: 120,
+            ac: true,
+          },
+        },
       },
     ],
+    baseline: {
+      baseline_id: 'id2',
+      baseline_name: 'bname',
+      duration_seconds: 1800,
+      intervals: [
+        {
+          duration_seconds: 1740,
+          signal_payloads: [50],
+          uid: '0',
+        },
+        {
+          duration_seconds: 60,
+          signal_payloads: [51],
+          uid: '1',
+        },
+      ],
+      item_base: {
+        type: 'power-real',
+        dis: 'RealPower',
+        units: 'W',
+        si_scale_code: 'none',
+        power_attributes: {
+          hertz: 60,
+          voltage: 120,
+          ac: true,
+        },
+      },
+      start_date: '2020-07-10T00:00:00Z',
+    },
   },
+  targets: [
+    {
+      dis: 'ven target',
+      target_type: 'ven',
+      value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+    },
+  ],
+  test_event: false,
+};
+
+const sampleVen1 = {
+  _id: '123123123123123123129999',
+  company: 'cyberdyne',
+  created_at: new Date(),
+  dis: 'Test VEN 1',
+  ns: 'cyberdyne_studios',
+  profile_name: '2.0b',
+  is_report_only: false,
+  supports_xml_sig: false,
+  transport_name: 'simpleHttp',
+  uses_http_pull: true,
+  client_certificate_common_name: 'aabbccddeeff',
+  client_certificate_fingerprint: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
 };
 
-const sampleReport1 = {
-  'D8:1D:4B:20:5A:65:4C:50:32:FA': {
-    venReportMetadata: [
+const sampleReport1 = [
+  {
+    report_request_ids: ['uuid0'],
+    report_specifier_id: 'TELEMETRY_STATUS',
+    descriptions: [
       {
-        reportRequestIds: ['uuid0'],
-        reportSpecifierId: 'TELEMETRY_STATUS',
-        descriptions: [
-          {
-            reportId: 'ts1',
-            reportType: 'x-resourceStatus',
-            readingType: 'x-notApplicable',
-            samplingRate: {
-              minPeriod: 'PT1M',
-              maxPeriod: 'PT1H',
-              onChange: false,
-            },
-          },
-        ],
-        lastReceivedRegister: '2020-04-26T01:00:00.000Z',
+        report_id: 'TelemetryStatusReport',
+        report_type: 'x-resourceStatus',
+        reading_type: 'x-notApplicable',
+        sampling_rate: {
+          min_period: 'PT1M',
+          max_period: 'PT1H',
+          on_change: false,
+        },
       },
+    ],
+    last_received_register: '2020-04-26T01:00:00.000Z',
+  },
+  {
+    report_request_ids: ['uuid1'],
+    report_specifier_id: 'TELEMETRY_USAGE',
+    descriptions: [
       {
-        reportRequestIds: ['uuid1'],
-        reportSpecifierId: 'TELEMETRY_USAGE',
-        descriptions: [
-          {
-            reportId: 'rep1',
-            reportType: 'usage',
-            readingType: 'Direct Read',
-            samplingRate: {
-              minPeriod: 'PT1M',
-              maxPeriod: 'PT1H',
-              onChange: false,
-            },
-          },
-          {
-            reportId: 'rep2',
-            reportType: 'usage',
-            readingType: 'Direct Read',
-            samplingRate: {
-              minPeriod: 'PT1M',
-              maxPeriod: 'PT1H',
-              onChange: false,
-            },
-          },
-        ],
-        lastReceivedRegister: '2020-04-26T01:00:00.000Z',
+        report_id: 'rep1',
+        report_type: 'usage',
+        reading_type: 'Direct Read',
+        sampling_rate: {
+          min_period: 'PT1M',
+          max_period: 'PT1H',
+          on_change: false,
+        },
+      },
+      {
+        report_id: 'rep2',
+        report_type: 'usage',
+        reading_type: 'Direct Read',
+        sampling_rate: {
+          min_period: 'PT1M',
+          max_period: 'PT1H',
+          on_change: false,
+        },
       },
     ],
+    last_received_register: '2020-04-26T01:00:00.000Z',
   },
-};
+];
 
 module.exports = {
   sampleEvent1,
   sampleReport1,
+  sampleVen1,
 };

+ 11 - 42
__tests__/unit/processes/event.spec.js

@@ -5,21 +5,21 @@ const { v4 } = require('uuid');
 const sinon = require('sinon');
 const rewire = require('rewire');
 
+const FakeNantumModule = require('../utils/fake-nantum-module');
+
 const {
   requestEventMax,
-  createdEventMax,
   createdEventInvalidEventId1,
 } = require('../xml/event/js-requests');
 
 const { poll } = require('../xml/poll/js-requests');
 
 const { generatedFromNantumEvent1 } = require('../xml/event/js-responses');
-const { sampleEvent1 } = require('../modules/nantum-responses');
+const { sampleEvent1, sampleVen1 } = require('../modules/nantum-responses');
 
 describe('Event', function() {
   let clock;
-  let sandbox, rewired;
-  let fetchEventStub;
+  let rewired;
 
   after(async () => {
     clock.restore();
@@ -27,25 +27,15 @@ describe('Event', function() {
 
   before(async () => {
     clock = sinon.useFakeTimers(new Date('2020-04-26T01:00:00.000Z').getTime());
-    sandbox = sinon.createSandbox();
 
-    let registration = {};
-    let opted = {};
+    const nantum = new FakeNantumModule({
+      events: [sampleEvent1],
+      vens: [sampleVen1],
+    });
 
-    fetchEventStub = sandbox.stub().resolves(sampleEvent1);
     rewired = rewire('../../../processes/event.js');
     rewired.__set__({
-      nantum: {
-        fetchRegistration: () => Promise.resolve(registration),
-        fetchEvent: fetchEventStub,
-        updateRegistration: async newRegistration => {
-          registration = newRegistration;
-        },
-        fetchOpted: venId => Promise.resolve(opted[venId] || []),
-        updateOpted: async (venId, newOpted) => {
-          opted[venId] = newOpted || [];
-        },
-      },
+      nantum,
     });
   });
 
@@ -65,27 +55,8 @@ describe('Event', function() {
     });
   });
 
-  describe('poll and updateOptType', function() {
-    it('should return the same event on subsequent polls if it has not been opted', async () => {
-      const venId = poll.venId;
-      const commonName = v4()
-        .replace(/-/g, '')
-        .substring(0, 12);
-      const pollResponse1 = await rewired.pollForEvents(
-        poll,
-        commonName,
-        venId,
-      );
-      expect(pollResponse1.events.length).to.eql(1);
-      const pollResponse2 = await rewired.pollForEvents(
-        poll,
-        commonName,
-        venId,
-      );
-      expect(pollResponse2.events.length).to.eql(1);
-    });
-
-    it('should not return an opted event in subsequent poll response', async () => {
+  describe('pollForEvents', function() {
+    it('should return an event only once', async () => {
       const venId = poll.venId;
       const commonName = v4()
         .replace(/-/g, '')
@@ -96,8 +67,6 @@ describe('Event', function() {
         venId,
       );
       expect(pollResponse1.events.length).to.eql(1);
-
-      await rewired.updateOptType(createdEventMax, commonName, venId);
       const pollResponse2 = await rewired.pollForEvents(
         poll,
         commonName,

+ 29 - 20
__tests__/unit/processes/registration.spec.js

@@ -2,17 +2,18 @@
 
 const { expect } = require('chai');
 const { v4 } = require('uuid');
-const { sequelize } = require('../../../db');
+const rewire = require('rewire');
 
-const {
-  cancelParty,
-  query,
-  registerParty,
-} = require('../../../processes/registration');
+const FakeNantumModule = require('../utils/fake-nantum-module');
 
 describe('VEN registration', function() {
+  let rewired;
+
   before(async () => {
-    await sequelize.sync();
+    rewired = rewire('../../../processes/registration.js');
+    rewired.__set__({
+      nantum: new FakeNantumModule(),
+    });
   });
 
   describe('registerParty', function() {
@@ -39,7 +40,11 @@ describe('VEN registration', function() {
         oadrVenName: `VEN ${commonName}`,
         oadrHttpPullModel: true,
       };
-      registrationResponse = await registerParty(request, commonName, venId);
+      registrationResponse = await rewired.registerParty(
+        request,
+        commonName,
+        venId,
+      );
     });
 
     it('allows registration of a new VEN', async () => {
@@ -70,7 +75,7 @@ describe('VEN registration', function() {
 
       let exception;
       try {
-        await registerParty(request, commonName, `${venId}:FF`);
+        await rewired.registerParty(request, commonName, `${venId}:FF`);
       } catch (e) {
         exception = e;
       }
@@ -97,7 +102,7 @@ describe('VEN registration', function() {
 
       let exception;
       try {
-        await registerParty(request, commonName2, venId);
+        await rewired.registerParty(request, commonName2, venId);
       } catch (e) {
         exception = e;
       }
@@ -124,7 +129,7 @@ describe('VEN registration', function() {
       const request = {
         requestId: requestId,
       };
-      queryResponse = await query(request, commonName, venId);
+      queryResponse = await rewired.query(request, commonName, venId);
     });
 
     it('does not return venId or registrationId for new device', async () => {
@@ -153,7 +158,7 @@ describe('VEN registration', function() {
         oadrVenName: `VEN ${commonName}`,
         oadrHttpPullModel: true,
       };
-      const registrationResponse = await registerParty(
+      const registrationResponse = await rewired.registerParty(
         registerRequest,
         commonName,
         venId,
@@ -163,7 +168,7 @@ describe('VEN registration', function() {
       const queryRequest = {
         requestId: requestId,
       };
-      queryResponse = await query(queryRequest, commonName, venId);
+      queryResponse = await rewired.query(queryRequest, commonName, venId);
       expect(queryResponse.registrationId).to.eql(initialRegistrationId);
       expect(queryResponse.venId).to.eql(venId);
     });
@@ -192,7 +197,7 @@ describe('VEN registration', function() {
 
       let lastError;
       try {
-        await registerParty(registerRequest, commonName, venId);
+        await rewired.registerParty(registerRequest, commonName, venId);
       } catch (e) {
         lastError = e;
       }
@@ -224,7 +229,7 @@ describe('VEN registration', function() {
 
       let lastError;
       try {
-        await registerParty(registerRequest, commonName, venId);
+        await rewired.registerParty(registerRequest, commonName, venId);
       } catch (e) {
         lastError = e;
       }
@@ -257,7 +262,11 @@ describe('VEN registration', function() {
         oadrVenName: `VEN ${commonName}`,
         oadrHttpPullModel: true,
       };
-      registrationResponse = await registerParty(request, commonName, venId);
+      registrationResponse = await rewired.registerParty(
+        request,
+        commonName,
+        venId,
+      );
     });
 
     it('successfully cancels an existing registration', async () => {
@@ -268,7 +277,7 @@ describe('VEN registration', function() {
         venId: venId,
       };
 
-      const cancelResponse = await cancelParty(
+      const cancelResponse = await rewired.cancelParty(
         cancelRequest,
         commonName,
         venId,
@@ -290,7 +299,7 @@ describe('VEN registration', function() {
 
       let error;
       try {
-        await cancelParty(cancelRequest, commonName, otherVenId);
+        await rewired.cancelParty(cancelRequest, commonName, otherVenId);
       } catch (e) {
         error = e;
       }
@@ -307,12 +316,12 @@ describe('VEN registration', function() {
       };
 
       // first cancellation
-      await cancelParty(cancelRequest, commonName, venId);
+      await rewired.cancelParty(cancelRequest, commonName, venId);
 
       let error;
       try {
         // second cancellation
-        await cancelParty(cancelRequest, commonName, venId);
+        await rewired.cancelParty(cancelRequest, commonName, venId);
       } catch (e) {
         error = e;
       }

+ 11 - 9
__tests__/unit/processes/report.spec.js

@@ -5,6 +5,9 @@ const { v4 } = require('uuid');
 const sinon = require('sinon');
 const rewire = require('rewire');
 
+const FakeNantumModule = require('../utils/fake-nantum-module');
+const { sampleEvent1, sampleVen1 } = require('../modules/nantum-responses');
+
 const {
   registerReportMax,
   createdReportGenerated1,
@@ -23,7 +26,7 @@ const { sampleReport1 } = require('../modules/nantum-responses');
 describe('Report', function() {
   let clock;
   let rewired;
-  let report;
+  let nantum;
   let uuidSequence;
 
   after(async () => {
@@ -36,16 +39,14 @@ describe('Report', function() {
   });
 
   before(async () => {
-    report = {};
+    nantum = new FakeNantumModule({
+      events: [sampleEvent1],
+      vens: [sampleVen1],
+    });
 
     rewired = rewire('../../../processes/report.js');
     rewired.__set__({
-      nantum: {
-        fetchReport: venId => Promise.resolve(report[venId] || []),
-        updateReport: async (venId, newReport) => {
-          report[venId] = newReport;
-        },
-      },
+      nantum,
       v4: () => `uuid${uuidSequence++}`,
     });
   });
@@ -63,7 +64,8 @@ describe('Report', function() {
         venId,
       );
       expect(registeredReport.responseCode).to.eql('200');
-      expect(report).to.eql(sampleReport1);
+      expect(nantum.vens.length).to.eql(1);
+      expect(nantum.vens[0].reports).to.eql(sampleReport1);
     });
 
     it('requests reports on next poll', async () => {

+ 81 - 0
__tests__/unit/utils/fake-nantum-module.js

@@ -0,0 +1,81 @@
+'use strict';
+
+const { v4 } = require('uuid');
+const _ = require('lodash');
+
+class FakeNantumModule {
+  constructor({ events, vens } = {}) {
+    this.vens = vens || [];
+    this.events = events || [];
+    this.eventResponses = [];
+    this.reportReadings = [];
+  }
+
+  async getVen(clientCertificateFingerprint) {
+    return _.find(this.vens, {
+      client_certificate_fingerprint: clientCertificateFingerprint,
+    });
+  }
+
+  async createVen(ven) {
+    const venId = v4();
+    const newVen = { ...ven, _id: venId };
+    this.vens.push(newVen);
+  }
+
+  async updateVen(venId, newProperties) {
+    const venIndex = _.findIndex(this.vens, { _id: venId });
+    if (venIndex == null)
+      throw new Error(`Could not find ven with _id: ${venId}`);
+    this.vens[venIndex] = { ...this.vens[venIndex], ...newProperties };
+  }
+
+  async getEventResponse(ven, eventId, modificationNumber) {
+    return _.find(this.eventResponses, {
+      ven_id: ven._id,
+      event_id: eventId,
+      modification_number: modificationNumber,
+    })[0];
+  }
+
+  async createEventResponse(eventResponse) {
+    const eventResponseId = v4();
+    const newEventResponse = { ...eventResponse, _id: eventResponseId };
+    this.eventResponses.push(newEventResponse);
+  }
+
+  async updateEventResponse(id, newProperties) {
+    const eventResponseIndex = _.findIndex(this.eventResponses, { _id: id });
+    if (eventResponseIndex == null)
+      throw new Error(`Could not find eventResponse with _id: ${id}`);
+    this.eventResponses[eventResponseIndex] = {
+      ...this.eventResponses[eventResponseIndex],
+      ...newProperties,
+    };
+  }
+
+  async markEventAsSeen(ven, eventId, modificationNumber) {
+    const existing = ven.seen_events || [];
+    const newVen = {
+      ...ven,
+      seen_events: [
+        ...existing,
+        {
+          event_id: eventId,
+          modification_number: modificationNumber,
+        },
+      ],
+    };
+    await this.updateVen(ven._id, newVen);
+  }
+
+  async getEvents() {
+    return [...this.events];
+  }
+
+  async sendReportReadings(ven, readings) {
+    this.reportReadings.push({ ven: ven._id, readings });
+  }
+}
+
+module.exports = FakeNantumModule;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
__tests__/unit/xml/event/created-event.spec.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 8 - 0
__tests__/unit/xml/event/distribute-event.spec.js


+ 5 - 5
__tests__/unit/xml/event/js-requests.js

@@ -31,8 +31,8 @@ const createdEventMin2 = {
       responseCode: '200',
       responseRequestId: '336f7e47b92eefe985ec',
       optType: 'optIn',
-      eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
-      modificationNumber: 2,
+      eventId: '5f0649904d122ec115231f69',
+      modificationNumber: 0,
     },
   ],
 };
@@ -49,8 +49,8 @@ const createdEventMax = {
       responseDescription: 'OK',
       responseRequestId: '336f7e47b92eefe985ec',
       optType: 'optIn',
-      eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
-      modificationNumber: 2,
+      eventId: '5f0649904d122ec115231f69',
+      modificationNumber: 0,
     },
   ],
 };
@@ -85,7 +85,7 @@ const createdEventOldModificationNumber1 = {
       responseDescription: 'OK',
       responseRequestId: '336f7e47b92eefe985ec',
       optType: 'optIn',
-      eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
+      eventId: '5f0649904d122ec115231f69',
       modificationNumber: 0,
     },
   ],

+ 173 - 102
__tests__/unit/xml/event/js-responses.js

@@ -43,9 +43,12 @@ const distributeEventMin2 = {
           },
         ],
       },
-      target: {
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+      targets: [
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
   ],
@@ -107,9 +110,12 @@ const distributeEventMax = {
             signalName: 'LOAD_CONTROL',
             signalType: 'x-loadControlCapacity',
             signalId: '64ba02508ab099d6eae6',
-            target: {
-              endDeviceAsset: ['Energy_Management_System'],
-            },
+            targets: [
+              {
+                type: 'end-device-asset',
+                value: 'Energy_Management_System',
+              },
+            ],
             currentValue: 0,
           },
           {
@@ -126,37 +132,41 @@ const distributeEventMax = {
             currentValue: 0,
           },
         ],
-        baseline: [
-          {
-            startDate: '2020-04-14T16:50:00.000Z',
-            duration: 'PT10M',
-            intervals: [
-              {
-                signalPayloads: [50],
-                duration: 'PT30M',
-                uid: '1',
-              },
-              {
-                signalPayloads: [60],
-                duration: 'PT30M',
-                uid: '2',
-              },
-            ],
-            baselineId: '72233284678ff05139f4',
-            baselineName: 'some baseline',
-            itemBase: {
-              type: 'currencyPerKWh',
-              description: 'currencyPerKWh',
-              units: 'USD',
-              siScaleCode: 'none',
+        baseline: {
+          startDate: '2020-04-14T16:50:00.000Z',
+          duration: 'PT10M',
+          intervals: [
+            {
+              signalPayloads: [50],
+              duration: 'PT30M',
+              uid: '1',
+            },
+            {
+              signalPayloads: [60],
+              duration: 'PT30M',
+              uid: '2',
             },
+          ],
+          baselineId: '72233284678ff05139f4',
+          baselineName: 'some baseline',
+          itemBase: {
+            type: 'currency-per-kwh',
+            description: 'currencyPerKWh',
+            units: 'USD',
+            siScaleCode: 'none',
           },
-        ],
-      },
-      target: {
-        groupId: ['Test Target'],
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+        },
+      },
+      targets: [
+        {
+          type: 'group',
+          value: 'Test Target',
+        },
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
     {
@@ -187,7 +197,7 @@ const distributeEventMax = {
             signalId: '38e550909d77bc37310d',
             currentValue: 0,
             itemBase: {
-              type: 'powerReal',
+              type: 'power-real',
               description: 'RealPower',
               units: 'W',
               siScaleCode: 'none',
@@ -213,9 +223,12 @@ const distributeEventMax = {
           },
         ],
       },
-      target: {
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+      targets: [
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
     {
@@ -255,9 +268,12 @@ const distributeEventMax = {
           },
         ],
       },
-      target: {
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+      targets: [
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
   ],
@@ -319,9 +335,12 @@ const distributeEventEpri1 = {
             signalName: 'LOAD_CONTROL',
             signalType: 'x-loadControlCapacity',
             signalId: '64ba02508ab099d6eae6',
-            target: {
-              endDeviceAsset: ['Energy_Management_System'],
-            },
+            targets: [
+              {
+                type: 'end-device-asset',
+                value: 'Energy_Management_System',
+              },
+            ],
             currentValue: 0,
           },
           {
@@ -338,37 +357,41 @@ const distributeEventEpri1 = {
             currentValue: 0,
           },
         ],
-        baseline: [
-          {
-            startDate: '2020-04-14T16:50:00.000Z',
-            duration: 'PT10M',
-            intervals: [
-              {
-                signalPayloads: [50],
-                duration: 'PT30M',
-                uid: '1',
-              },
-              {
-                signalPayloads: [60],
-                duration: 'PT30M',
-                uid: '2',
-              },
-            ],
-            baselineId: '72233284678ff05139f4',
-            baselineName: 'some baseline',
-            itemBase: {
-              type: 'currencyPerKWh',
-              description: 'currencyPerKWh',
-              units: 'USD',
-              siScaleCode: 'none',
+        baseline: {
+          startDate: '2020-04-14T16:50:00.000Z',
+          duration: 'PT10M',
+          intervals: [
+            {
+              signalPayloads: [50],
+              duration: 'PT30M',
+              uid: '1',
             },
+            {
+              signalPayloads: [60],
+              duration: 'PT30M',
+              uid: '2',
+            },
+          ],
+          baselineId: '72233284678ff05139f4',
+          baselineName: 'some baseline',
+          itemBase: {
+            type: 'currency-per-kwh',
+            description: 'currencyPerKWh',
+            units: 'USD',
+            siScaleCode: 'none',
           },
-        ],
-      },
-      target: {
-        groupId: ['Test Target'],
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+        },
+      },
+      targets: [
+        {
+          type: 'group',
+          value: 'Test Target',
+        },
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
     {
@@ -399,7 +422,7 @@ const distributeEventEpri1 = {
             signalId: '38e550909d77bc37310d',
             currentValue: 0,
             itemBase: {
-              type: 'powerReal',
+              type: 'power-real',
               description: 'RealPower',
               units: 'W',
               siScaleCode: 'none',
@@ -425,9 +448,12 @@ const distributeEventEpri1 = {
           },
         ],
       },
-      target: {
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+      targets: [
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
     {
@@ -467,15 +493,19 @@ const distributeEventEpri1 = {
           },
         ],
       },
-      target: {
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+      targets: [
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
   ],
 };
 
 const generatedFromNantumEvent1 = {
+  _type: 'oadrDistributeEvent',
   responseCode: '200',
   responseDescription: 'OK',
   responseRequestId: '2233',
@@ -484,43 +514,84 @@ const generatedFromNantumEvent1 = {
   events: [
     {
       eventDescriptor: {
-        eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
-        modificationNumber: 2,
-        marketContext: 'http://MarketContext1',
-        createdDateTime: '2020-04-14T16:06:39.000Z',
-        eventStatus: 'far',
+        eventId: '5f076b8e4d122ec1152361ec',
+        modificationNumber: 0,
+        modificationReason: undefined,
+        modificationDateTime: undefined,
+        marketContext: 'http://emix',
+        createdDateTime: '2020-07-09T19:10:06.332Z',
+        vtnComment: 'Test Event 1',
+        eventStatus: 'none',
         testEvent: false,
         priority: 0,
       },
       activePeriod: {
-        duration: 'PT3300S',
-        notificationDuration: 'PT87000S',
-        startDate: '2020-04-26T23:00:00.000Z',
+        startDate: '2020-07-10T00:00:00Z',
+        duration: 'PT1800S',
+        notificationDuration: 'PT86400S',
+        toleranceTolerateStartAfter: 'PT0S',
+        rampUpDuration: 'PT3600S',
+        recoveryDuration: undefined,
       },
       signals: {
         event: [
           {
-            signalName: 'LOAD_AMOUNT',
-            signalId: '112233445566',
-            signalType: 'level',
+            signalName: 'BID_LOAD',
+            signalId: 'id1',
+            signalType: 'setpoint',
+            currentValue: 45.5,
+            duration: 'PT1800S',
+            startDate: '2020-07-10T00:00:00.000Z',
             intervals: [
               {
-                signalPayloads: [41],
-                duration: 'PT10S',
-                uid: '1',
+                signalPayloads: [50],
+                duration: 'PT1740S',
+                uid: '0',
               },
               {
-                signalPayloads: [42],
-                duration: 'PT3290S',
-                uid: '2',
+                signalPayloads: [51],
+                duration: 'PT60S',
+                uid: '1',
               },
             ],
+            itemBase: {
+              type: 'power-real',
+              description: 'RealPower',
+              units: 'W',
+              siScaleCode: 'none',
+              powerAttributes: {
+                hertz: 60,
+                voltage: 120,
+                ac: true,
+              },
+            },
           },
         ],
-      },
-      target: {
-        venId: ['D8:1D:4B:20:5A:65:4C:50:32:FA'],
-      },
+        baseline: {
+          baselineName: 'bname',
+          baselineId: 'id2',
+          duration: 'PT1800S',
+          startDate: '2020-07-10T00:00:00Z',
+          intervals: [
+            {
+              signalPayloads: [50],
+              duration: 'PT1740S',
+              uid: '0',
+            },
+            {
+              signalPayloads: [51],
+              duration: 'PT60S',
+              uid: '1',
+            },
+          ],
+        },
+      },
+      targets: [
+        {
+          type: 'ven',
+          value: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+        },
+      ],
       responseRequired: 'always',
     },
   ],

+ 5 - 5
__tests__/unit/xml/event/xml-requests.js

@@ -63,8 +63,8 @@ const createdEventMin2Xml = `<oadr2b:oadrPayload xmlns:oadr2b="http://openadr.or
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <ei:optType>optIn</ei:optType>
       <ei:qualifiedEventID>
-       <ei:eventID>a2fa542eca8d4e829ff5c0f0c8e68710</ei:eventID>
-       <ei:modificationNumber>2</ei:modificationNumber>
+       <ei:eventID>5f0649904d122ec115231f69</ei:eventID>
+       <ei:modificationNumber>0</ei:modificationNumber>
       </ei:qualifiedEventID>
      </ei:eventResponse>
     </ei:eventResponses>
@@ -90,8 +90,8 @@ const createdEventMaxXml = `<oadr2b:oadrPayload xmlns:oadr2b="http://openadr.org
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <ei:optType>optIn</ei:optType>
       <ei:qualifiedEventID>
-       <ei:eventID>a2fa542eca8d4e829ff5c0f0c8e68710</ei:eventID>
-       <ei:modificationNumber>2</ei:modificationNumber>
+       <ei:eventID>5f0649904d122ec115231f69</ei:eventID>
+       <ei:modificationNumber>0</ei:modificationNumber>
       </ei:qualifiedEventID>
      </ei:eventResponse>
     </ei:eventResponses>
@@ -115,7 +115,7 @@ const createdEventMissingRequiredXml = `<oadr2b:oadrPayload xmlns:oadr2b="http:/
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <ei:optType>optIn</ei:optType>
       <ei:qualifiedEventID>
-       <ei:eventID>a2fa542eca8d4e829ff5c0f0c8e68710</ei:eventID>
+       <ei:eventID>5f0649904d122ec115231f69</ei:eventID>
       </ei:qualifiedEventID>
      </ei:eventResponse>
     </ei:eventResponses>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
__tests__/unit/xml/report/create-report.spec.js


+ 1 - 1
__tests__/unit/xml/report/created-report.spec.js

@@ -7,7 +7,7 @@ const {
   createdReportMinXml,
   createdReportMaxXml,
   createdReportMissingRequiredXml,
-  createdReportErrorXml
+  createdReportErrorXml,
 } = require('./xml-requests');
 const { createdReportMin, createdReportMax } = require('./js-requests');
 

+ 2 - 2
__tests__/unit/xml/report/js-requests.js

@@ -19,7 +19,7 @@ const registerReportMax = {
       reportName: 'METADATA_TELEMETRY_STATUS',
       descriptions: [
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           reportType: 'x-resourceStatus',
           readingType: 'x-notApplicable',
           samplingRate: {
@@ -73,7 +73,7 @@ const registerReportCa = {
       reportName: 'METADATA_TELEMETRY_STATUS',
       descriptions: [
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           reportType: 'x-resourceStatus',
           readingType: 'x-notApplicable',
           samplingRate: {

+ 4 - 4
__tests__/unit/xml/report/js-responses.js

@@ -39,7 +39,7 @@ const registeredReportMax = {
       duration: 'PT24H',
       specifiers: [
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
         },
       ],
@@ -84,7 +84,7 @@ const createReportMax = {
       duration: 'PT24H',
       specifiers: [
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
         },
       ],
@@ -105,7 +105,7 @@ const createReportGenerated1 = {
       duration: 'PT3600S',
       specifiers: [
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
         },
       ],
@@ -144,7 +144,7 @@ const createReportGenerated2 = {
       duration: 'PT3600S',
       specifiers: [
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
         },
       ],

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
__tests__/unit/xml/report/register-report.spec.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
__tests__/unit/xml/report/registered-report.spec.js


+ 3 - 3
__tests__/unit/xml/report/xml-requests.js

@@ -67,7 +67,7 @@ const registerReportMaxXml = `<oadr2b:oadrPayload xmlns:oadr2b="http://openadr.o
      <xcal:duration>PT1H</xcal:duration>
     </xcal:duration>
     <oadr2b:oadrReportDescription>
-     <ei:rID>ts1</ei:rID>
+     <ei:rID>TelemetryStatusReport</ei:rID>
      <ei:reportType>x-resourceStatus</ei:reportType>
      <ei:readingType>x-notApplicable</ei:readingType>
      <oadr2b:oadrSamplingRate>
@@ -130,7 +130,7 @@ const registerReportMissingRequiredXml = `<oadr2b:oadrPayload xmlns:oadr2b="http
      <xcal:duration>PT1H</xcal:duration>
     </xcal:duration>
     <oadr2b:oadrReportDescription>
-     <ei:rID>ts1</ei:rID>
+     <ei:rID>TelemetryStatusReport</ei:rID>
      <ei:reportType>x-resourceStatus</ei:reportType>
      <ei:readingType>x-notApplicable</ei:readingType>
      <oadr2b:oadrSamplingRate>
@@ -178,7 +178,7 @@ const registerReportCaXml = `<oadr2b:oadrPayload xmlns:oadr2b="http://openadr.or
 <xcal:duration>PT1H</xcal:duration>
 </xcal:duration>
 <oadr2b:oadrReportDescription>
-<ei:rID>ts1</ei:rID>
+<ei:rID>TelemetryStatusReport</ei:rID>
 <ei:reportType>x-resourceStatus</ei:reportType>
 <ei:readingType>x-notApplicable</ei:readingType>
 <oadr2b:oadrSamplingRate>

+ 3 - 3
__tests__/unit/xml/report/xml-responses.js

@@ -55,7 +55,7 @@ const createReportMaxXml = `<ns2:oadrPayload xmlns="http://www.w3.org/2000/09/xm
       </ns5:properties>
      </ns3:reportInterval>
      <ns3:specifierPayload>
-      <ns3:rID>ts1</ns3:rID>
+      <ns3:rID>TelemetryStatusReport</ns3:rID>
       <ns3:readingType>x-notApplicable</ns3:readingType>
      </ns3:specifierPayload>
     </ns3:reportSpecifier>
@@ -96,7 +96,7 @@ const createReportMissingRequiredXml = `<ns2:oadrPayload xmlns="http://www.w3.or
       </ns5:properties>
      </ns3:reportInterval>
      <ns3:specifierPayload>
-      <ns3:rID>ts1</ns3:rID>
+      <ns3:rID>TelemetryStatusReport</ns3:rID>
       <ns3:readingType>x-notApplicable</ns3:readingType>
      </ns3:specifierPayload>
     </ns3:reportSpecifier>
@@ -164,7 +164,7 @@ const registeredReportMaxXml = `<ns2:oadrPayload xmlns="http://www.w3.org/2000/0
       </ns5:properties>
      </ns3:reportInterval>
      <ns3:specifierPayload>
-      <ns3:rID>ts1</ns3:rID>
+      <ns3:rID>TelemetryStatusReport</ns3:rID>
       <ns3:readingType>x-notApplicable</ns3:readingType>
      </ns3:specifierPayload>
     </ns3:reportSpecifier>

+ 3 - 4
config/development.js

@@ -1,7 +1,8 @@
 'use strict';
 
 module.exports = {
-  port: process.env.PORT || 3000,
+  company: process.env.COMPANY,
+  nantumUrl: process.env.NANTUM_URL,
   noAWS: process.env.NO_AWS,
   loggerOptions: {
     cache: {
@@ -12,8 +13,6 @@ module.exports = {
       company: process.env.COMPANY || 'company_not_provided',
     },
   },
-  dbConfig: {
-    uri: process.env.DB_URL,
-  },
+  port: process.env.PORT || 3000,
   vtnId: 'NANTUM_VTN',
 };

+ 2 - 3
config/production.js

@@ -1,6 +1,8 @@
 'use strict';
 
 module.exports = {
+  company: process.env.COMPANY,
+  nantumUrl: process.env.NANTUM_URL,
   port: process.env.PORT || 3001,
   loggerOptions: {
     cache: {
@@ -11,8 +13,5 @@ module.exports = {
       company: process.env.COMPANY || 'company_not_provided',
     },
   },
-  dbConfig: {
-    uri: process.env.DB_URL,
-  },
   vtnId: 'NANTUM_VTN',
 };

+ 2 - 3
config/testing.js

@@ -1,6 +1,8 @@
 'use strict';
 
 module.exports = {
+  company: process.env.COMPANY || 'test_company',
+  nantumUrl: process.env.NANTUM_URL,
   port: 3002,
   loggerOptions: {
     cache: {
@@ -11,8 +13,5 @@ module.exports = {
       company: process.env.COMPANY || 'company_not_provided',
     },
   },
-  dbConfig: {
-    uri: process.env.DB_URL,
-  },
   vtnId: 'TEST_VTN',
 };

+ 0 - 15
db/_db.js

@@ -1,15 +0,0 @@
-'use strict';
-
-const Sequelize = require('sequelize');
-
-const {
-  dbConfig: { uri },
-} = require('../config');
-
-let sequelize;
-
-if (!sequelize) {
-  sequelize = new Sequelize(uri, { logging: false });
-}
-
-module.exports = sequelize;

+ 0 - 10
db/index.js

@@ -1,10 +0,0 @@
-'use strict';
-
-const sequelize = require('./_db');
-
-const { Ven } = require('./models');
-
-module.exports = {
-  sequelize,
-  Ven,
-};

+ 0 - 7
db/models/index.js

@@ -1,7 +0,0 @@
-'use strict';
-
-const Ven = require('./ven');
-
-module.exports = {
-  Ven: Ven,
-};

+ 0 - 18
db/models/ven.js

@@ -1,18 +0,0 @@
-'use strict';
-
-const Sequelize = require('sequelize');
-const sequelize = require('../_db');
-
-const Ven = sequelize.define('Ven', {
-  common_name: {
-    type: Sequelize.STRING,
-    allowNull: false,
-  },
-  ven_id: {
-    type: Sequelize.STRING,
-    allowNull: false,
-  },
-  data: Sequelize.JSON,
-});
-
-module.exports = Ven;

+ 0 - 16
docker-compose.yml

@@ -16,21 +16,6 @@ services:
       - nodejs
     restart: unless-stopped
 
-  db:
-    container_name: nantum-vtn-db
-    expose:
-      - 5432
-#    ports:
-#      - 55432:5432
-    image: postgres:9.5
-    volumes:
-      - postgres_data:/var/lib/postgresql/data
-    environment:
-      POSTGRES_DB: vtn_test
-      POSTGRES_USER: vtn
-      POSTGRES_PASSWORD: vtn
-    restart: unless-stopped
-
   nodejs:
     container_name: nantum-vtn-nodejs
     build: .
@@ -39,7 +24,6 @@ services:
     restart: on-failure
     environment:
       NODE_ENV: development
-      DB_URL: postgres://vtn:vtn@nantum-vtn-db:5432/vtn_test
       NO_AWS: 'true'
       PORT: 8080
 

+ 0 - 3
docker_run_psql.sh

@@ -1,3 +0,0 @@
-#!/bin/bash
-
-docker-compose exec -u postgres db psql -U vtn vtn_test

+ 1 - 1
docker_run_tests.sh

@@ -2,4 +2,4 @@
 
 set -e
 
-docker-compose run --rm -e DB_URL=postgres://vtn:vtn@nantum-vtn-db:5432/vtn_test nodejs npm test
+docker-compose run --rm -e nodejs npm test

+ 0 - 2
index.js

@@ -1,13 +1,11 @@
 'use strict';
 
 const logger = require('./logger');
-const { sequelize } = require('./db');
 
 logger.info('Starting OADR VTN...');
 
 (async () => {
   try {
-    await sequelize.sync();
     await require('./server').start();
     logger.info('OADR VTN up and running');
   } catch (e) {

+ 79 - 92
modules/nantum.js

@@ -1,120 +1,107 @@
 'use strict';
 
-const { Ven } = require('../db');
+const Nantum = require('@hw/edge-sdks');
+const { API } = Nantum;
+const logger = require('../logger');
+const { company, nantumUrl } = require('../config');
 
-async function fetchEvent(venId) {
-  return {
-    event_identifier: 'aa2233eca8d4e829ff5c0f0c8e68710',
-    client_id: venId,
-    test_event: false,
-    event_mod_number: 0,
-    offLine: false,
-    dr_mode_data: {
-      operation_mode_value: 'NORMAL',
-      // currentTime: 'xxxxx', //TODO: find reasonable value
-    },
-    dr_event_data: {
-      notification_time: '2020-05-14T00:00:00.000Z',
-      start_time: '2020-05-15T14:00:00.000Z',
-      end_time: '2020-05-15T15:55:00.000Z',
-      event_instance: [
-        {
-          event_type_id: 'LOAD_DISPATCH',
-          event_info_values: [
-            { value: 41, timeOffset: 0 },
-            { value: 42, timeOffset: 5 * 60 },
-            { value: 43, timeOffset: 10 * 60 },
-          ],
-        },
-      ],
+const { request } = API({ company, logger });
+
+async function getVen(clientCertificateFingerprint) {
+  const results = await request({
+    uri: `${nantumUrl}/oadr_vens`,
+    query: {
+      client_certificate_fingerprint: clientCertificateFingerprint,
     },
-  };
+  });
+  return results[0];
 }
 
-async function fetchRegistration(venId) {
-  const venRecord = await Ven.findOne({
-    where: { ven_id: venId },
+async function createVen(ven) {
+  await request({
+    method: 'POST',
+    uri: `${nantumUrl}/oadr_vens`,
+    body: ven,
   });
-  if (venRecord) return venRecord.data.registration;
 }
 
-async function fetchReport(venId) {
-  const venRecord = await Ven.findOne({
-    where: { ven_id: venId },
+async function updateVen(id, newProperties) {
+  await request({
+    method: 'PUT',
+    uri: `${nantumUrl}/oadr_vens/${id}`,
+    body: newProperties,
   });
-  if (venRecord && venRecord.data.report) return venRecord.data.report;
-  return {};
 }
 
-async function fetchOpted(venId) {
-  const venRecord = await Ven.findOne({
-    where: { ven_id: venId },
+async function getEventResponse(ven, eventId, modificationNumber) {
+  const results = await request({
+    uri: `${nantumUrl}/oadr_event_responses`,
+    query: {
+      ven_id: ven._id,
+      event_id: eventId,
+      modification_number: modificationNumber,
+    },
   });
-  if (venRecord && venRecord.data.opted) return venRecord.data.opted;
-  return [];
+  return results[0];
 }
 
-async function updateRegistration(registration) {
-  if (registration.ven_id == null) {
-    throw new Error('Registration is missing ven_id');
-  }
-  if (registration.common_name == null) {
-    throw new Error('Registration is missing common_name');
-  }
-  let venRecord = await Ven.findOne({
-    where: { ven_id: registration.ven_id },
+async function createEventResponse(eventResponse) {
+  await request({
+    method: 'POST',
+    uri: `${nantumUrl}/oadr_event_responses`,
+    body: eventResponse,
   });
-
-  if (venRecord) {
-    const newData = venRecord.data || {};
-    newData.registration = registration;
-    venRecord.set('data', newData); // setting `data` directly on object doesn't trigger change detection
-  } else {
-    venRecord = new Ven();
-    venRecord.ven_id = registration.ven_id;
-    venRecord.common_name = registration.common_name;
-    const newData = { registration: registration };
-    venRecord.set('data', newData);
-  }
-  await venRecord.save();
 }
 
-async function updateOpted(venId, opted) {
-  let venRecord = await Ven.findOne({
-    where: { ven_id: venId },
+async function updateEventResponse(id, newProperties) {
+  await request({
+    method: 'PUT',
+    uri: `${nantumUrl}/oadr_event_responses/${id}`,
+    body: newProperties,
   });
+}
 
-  if (venRecord) {
-    const newData = venRecord.data || {};
-    newData.opted = opted;
-    venRecord.set('data', newData); // setting `data` directly on object doesn't trigger change detection
-  } else {
-    throw new Error(`Ven ${venId} must be registered`);
-  }
-  await venRecord.save();
+async function markEventAsSeen(ven, eventId, modificationNumber) {
+  //TODO: potentially racy. Consider breaking out `seen_events` into its own document.
+  const existing = ven.seen_events || [];
+  await request({
+    method: 'PUT',
+    uri: `${nantumUrl}/oadr_vens/${ven._id}`,
+    body: {
+      seen_events: [
+        ...existing,
+        {
+          event_id: eventId,
+          modification_number: modificationNumber,
+        },
+      ],
+    },
+  });
 }
 
-async function updateReport(venId, report) {
-  let venRecord = await Ven.findOne({
-    where: { ven_id: venId },
+async function getEvents() {
+  return await request({
+    uri: `${nantumUrl}/oadr_events`,
+    query: {},
   });
+}
 
-  if (venRecord) {
-    const newData = venRecord.data || {};
-    newData.report = report;
-    venRecord.set('data', newData); // setting `data` directly on object doesn't trigger change detection
-  } else {
-    throw new Error(`Ven ${venId} must be registered`);
-  }
-  await venRecord.save();
+async function sendReportReadings(ven, readings) {
+  logger.info(
+    'received report readings',
+    ven._id,
+    JSON.stringify(readings, null, 2),
+  );
 }
 
 module.exports = {
-  fetchEvent,
-  fetchOpted,
-  fetchRegistration,
-  fetchReport,
-  updateRegistration,
-  updateReport,
-  updateOpted,
+  getEvents,
+  getEventResponse,
+  createEventResponse,
+  updateEventResponse,
+  markEventAsSeen,
+  getVen,
+  updateVen,
+  createVen,
+  sendReportReadings,
 };

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 717 - 241
package-lock.json


+ 7 - 7
package.json

@@ -7,27 +7,27 @@
     "start": "node index.js",
     "test": "npm run unit",
     "unit": "NODE_ENV=test _mocha --exit $(find __tests__ -name \"*.spec.js\")",
-    "lint": "eslint $(find __tests__ client config db modules processes server xml -name \"*.js\")",
-    "fixlint": "eslint --fix $(find __tests__ client config db modules processes server xml -name \"*.js\")",
-    "fixprettier": "prettier --write $(find __tests__ client config db modules processes server xml -name \"*.js\")"
+    "lint": "eslint $(find __tests__ client config modules processes server xml -name \"*.js\")",
+    "fixlint": "eslint --fix $(find __tests__ client config modules processes server xml -name \"*.js\")",
+    "fixprettier": "prettier --write $(find __tests__ client config modules processes server xml -name \"*.js\")"
   },
   "repository": {
     "type": "git",
     "url": "git+https://cdemoll@bitbucket.org/HWPD/cloud_oadr-vtn.git"
   },
   "dependencies": {
+    "@hw/edge-sdks": "^2.0.17",
+    "@hw/kinesis-logger": "^1.0.3",
     "axios": "^0.19.2",
     "bluebird": "^3.7.2",
     "body-parser": "^1.19.0",
     "eslint": "^5.16.0",
     "express": "^4.16.4",
+    "lodash": "^4.17.15",
     "node-forge": "^0.9.1",
-    "pg": "^7.18.2",
-    "sequelize": "^5.21.6",
     "uuid": "^7.0.3",
     "xml2js": "^0.4.23",
-    "xmlbuilder2": "^2.1.1",
-    "@hw/kinesis-logger": "^1.0.3"
+    "xmlbuilder2": "^2.1.1"
   },
   "devDependencies": {
     "chai": "^4.2.0",

+ 190 - 130
processes/event.js

@@ -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;

+ 41 - 41
processes/registration.js

@@ -21,32 +21,40 @@ async function registerParty(
   validateVenId(requestVenId, clientCertificateFingerprint, true);
   validateCreatePartyRegistration(oadrCreatePartyRegistration);
 
-  let nantumRegistration = await nantum.fetchRegistration(requestVenId);
+  let ven = await nantum.getVen(requestVenId);
 
-  if (nantumRegistration) {
-    if (nantumRegistration.common_name !== clientCertificateCn) {
+  if (ven) {
+    if (ven.client_certificate_common_name !== clientCertificateCn) {
       const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       throw error;
     }
-    if (nantumRegistration.registration_id == null) {
+    if (ven.registration_id == null) {
       const registrationId = v4().replace(/-/g, '');
-      nantumRegistration.registration_id = registrationId;
-      await nantum.updateRegistration(nantumRegistration);
+      await nantum.updateVen(ven._id, {
+        registration_id: registrationId,
+      });
     }
   } else {
     const registrationId = v4().replace(/-/g, '');
-    nantumRegistration = {
-      common_name: clientCertificateCn,
-      ven_id: requestVenId,
+
+    ven = {
+      client_certificate_common_name: clientCertificateCn,
+      client_certificate_fingerprint: clientCertificateFingerprint,
       registration_id: registrationId,
+      is_report_only: oadrCreatePartyRegistration.oadrReportOnly,
+      profile_name: oadrCreatePartyRegistration.oadrProfileName,
+      supports_xml_sig: oadrCreatePartyRegistration.oadrXmlSignature,
+      transport_name: oadrCreatePartyRegistration.oadrTransportName,
+      uses_http_pull: oadrCreatePartyRegistration.oadrHttpPullModel,
+      dis: oadrCreatePartyRegistration.oadrVenName,
     };
-    await nantum.updateRegistration(nantumRegistration);
+    await nantum.createVen(ven);
   }
 
-  return nantumRegistrationToOadrRegistrationCreated(
+  return venToOadrRegistrationCreated(
     oadrCreatePartyRegistration.requestId,
-    nantumRegistration,
+    ven,
   );
 }
 
@@ -105,23 +113,20 @@ async function query(
 
   const requestVenId = clientCertificateFingerprint;
 
-  let nantumRegistration = await nantum.fetchRegistration(requestVenId);
+  let ven = await nantum.getVen(requestVenId);
 
-  if (nantumRegistration) {
-    if (nantumRegistration.common_name !== clientCertificateCn) {
+  if (ven) {
+    if (ven.client_certificate_common_name !== clientCertificateCn) {
       const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       throw error;
     }
   } else {
     // response payload should not contain ven_id or registration_id
-    nantumRegistration = {};
+    ven = {};
   }
 
-  return nantumRegistrationToOadrRegistrationCreated(
-    oadrQueryRegistration.requestId,
-    nantumRegistration,
-  );
+  return venToOadrRegistrationCreated(oadrQueryRegistration.requestId, ven);
 }
 
 async function cancelParty(
@@ -140,30 +145,28 @@ async function cancelParty(
   validateVenId(requestVenId, clientCertificateFingerprint, false);
   const venId = clientCertificateFingerprint;
 
-  let nantumRegistration = await nantum.fetchRegistration(requestVenId);
+  let ven = await nantum.getVen(requestVenId);
+
   let cancelledRegistrationId;
 
-  if (nantumRegistration) {
-    if (nantumRegistration.common_name !== clientCertificateCn) {
+  if (ven) {
+    if (ven.client_certificate_common_name !== clientCertificateCn) {
       const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       throw error;
     }
 
-    cancelledRegistrationId = nantumRegistration.registration_id;
+    cancelledRegistrationId = ven.registration_id;
+    if (cancelledRegistrationId == null) {
+      const error = new Error('No current registration for VenID');
+      error.responseCode = 452;
+      throw error;
+    }
 
     // clear all registration data
-    nantumRegistration = {
-      ven_id: requestVenId,
-      common_name: clientCertificateCn,
-    };
-    await nantum.updateRegistration(nantumRegistration);
-  }
-
-  if (cancelledRegistrationId == null) {
-    const error = new Error('No current registration for VenID');
-    error.responseCode = 452;
-    throw error;
+    await nantum.updateVen(ven._id, {
+      registration_id: null,
+    });
   }
 
   return {
@@ -175,16 +178,13 @@ async function cancelParty(
   };
 }
 
-function nantumRegistrationToOadrRegistrationCreated(
-  requestId,
-  nantumRegistration,
-) {
+function venToOadrRegistrationCreated(requestId, ven) {
   return {
     responseRequestId: requestId || '',
     responseCode: '200',
     responseDescription: 'OK',
-    registrationId: nantumRegistration.registration_id,
-    venId: nantumRegistration.ven_id,
+    registrationId: ven.registration_id,
+    venId: ven.client_certificate_fingerprint,
     vtnId: vtnId,
     pollFreqDuration: 'PT10S',
   };

+ 115 - 66
processes/report.js

@@ -1,15 +1,16 @@
 'use strict';
+const _ = require('lodash');
+const { v4 } = require('uuid');
 
 const logger = require('../logger');
 const nantum = require('../modules/nantum');
-const { v4 } = require('uuid');
 
 const reportSubscriptionParameters = {
   dataGranularitySeconds: 60,
   reportBackSeconds: 60,
   subscriptionDurationSeconds: 60 * 60,
   resubscribeDurationSeconds: 59 * 60,
-  resubscribeAfterNoDataForSeconds: 90
+  resubscribeAfterNoDataForSeconds: 90,
 };
 
 function getSecondsSince(property, reportMetadata) {
@@ -22,6 +23,7 @@ function getSecondsSince(property, reportMetadata) {
   }
 }
 
+// this is called every time the VEN polls. Check to see if we have any stale subscriptions and resubscribe as needed.
 async function pollForReports(
   oadrPoll,
   clientCertificateCn,
@@ -34,30 +36,41 @@ async function pollForReports(
     clientCertificateFingerprint,
   );
 
-  const report = await nantum.fetchReport(clientCertificateFingerprint);
+  const ven = await nantum.getVen(clientCertificateFingerprint);
+  if (!ven) {
+    // not an error, we shouldn't fail polling because the VEN hasn't registered yet
+    return;
+  }
+
   const createRequests = [];
 
-  if (report.venReportMetadata) {
-    for (const reportMetadata of report.venReportMetadata) {
+  if (ven.reports) {
+    for (const reportMetadata of ven.reports) {
       let sendCreate = false;
 
-      if (!reportMetadata.lastSentCreate) {
+      if (!reportMetadata.last_sent_create) {
         // if we've never sent a subscription request, do it
-        logger.info('sending create because we never have', reportMetadata.reportSpecifierId);
+        logger.info(
+          'sending create because we never have',
+          reportMetadata.report_specifier_id,
+        );
         sendCreate = true;
       } else {
         // have sent a create > 5s ago, not received a created
         if (
-          !reportMetadata.lastReceivedCreated &&
-          getSecondsSince('lastSentCreate', reportMetadata) > 5
+          !reportMetadata.last_received_created &&
+          getSecondsSince('last_sent_create', reportMetadata) > 5
         ) {
-          logger.info('no reply to creation request, send another', reportMetadata.reportSpecifierId);
+          logger.info(
+            'no reply to creation request, send another',
+            reportMetadata.report_specifier_id,
+          );
           sendCreate = true;
         }
       }
 
       if (
-        getSecondsSince('lastReceivedUpdate', reportMetadata) >
+        getSecondsSince('last_received_update', reportMetadata) >
         reportSubscriptionParameters.resubscribeAfterNoDataForSeconds
       ) {
         // previously received data, silent now
@@ -65,44 +78,50 @@ async function pollForReports(
       }
 
       if (
-        !reportMetadata.lastReceivedUpdate &&
-        getSecondsSince('lastReceivedCreated', reportMetadata) >
+        !reportMetadata.last_received_update &&
+        getSecondsSince('last_received_created', reportMetadata) >
           reportSubscriptionParameters.reportBackSeconds + 5
       ) {
         // if we haven't received any data but we've waited long enough for one data interval + 5 seconds
-        logger.info('sending create because have not received data', reportMetadata.reportSpecifierId);
+        logger.info(
+          'sending create because have not received data',
+          reportMetadata.report_specifier_id,
+        );
         sendCreate = true;
       }
 
       if (
-        getSecondsSince('lastReceivedCreated', reportMetadata) >
+        getSecondsSince('last_received_created', reportMetadata) >
         reportSubscriptionParameters.resubscribeDurationSeconds
       ) {
         // when we're close to the end of the subscription, trigger a resubscribe
-        logger.info('sending create because close to end of subscription', reportMetadata.reportSpecifierId);
+        logger.info(
+          'sending create because close to end of subscription',
+          reportMetadata.report_specifier_id,
+        );
         sendCreate = true;
       }
 
       if (sendCreate) {
         const newReportRequestId = v4();
         // track the last 10 registration ids
-        reportMetadata.reportRequestIds = [
+        reportMetadata.report_request_ids = [
           newReportRequestId,
-          ...reportMetadata.reportRequestIds,
+          ...reportMetadata.report_request_ids,
         ].slice(0, 10);
         createRequests.push({
           reportRequestId: newReportRequestId,
-          reportSpecifierId: reportMetadata.reportSpecifierId,
+          reportSpecifierId: reportMetadata.report_specifier_id,
           granularityDuration: `PT${reportSubscriptionParameters.dataGranularitySeconds}S`,
           reportBackDuration: `PT${reportSubscriptionParameters.reportBackSeconds}S`,
           startDate: new Date().toISOString(),
           duration: `PT${reportSubscriptionParameters.subscriptionDurationSeconds}S`,
           specifiers: reportMetadata.descriptions.map(description => ({
-            reportId: description.reportId,
+            reportId: description.report_id,
             readingType: 'x-notApplicable',
           })),
         });
-        reportMetadata.lastSentCreate = new Date().toISOString();
+        reportMetadata.last_sent_create = new Date().toISOString();
       }
     }
     if (createRequests.length > 0) {
@@ -111,7 +130,10 @@ async function pollForReports(
         requestId: v4(),
         requests: createRequests,
       };
-      await nantum.updateReport(clientCertificateFingerprint, report);
+
+      await nantum.updateVen(ven._id, {
+        reports: ven.reports,
+      });
       return createReport;
     }
   }
@@ -131,23 +153,38 @@ async function registerReports(
 
   const requestVenId = oadrRegisterReport.venId;
   validateVenId(requestVenId, clientCertificateFingerprint, false);
+  const ven = await nantum.getVen(clientCertificateFingerprint);
+  if (!ven) {
+    throw new Error('VEN is not registered');
+  }
 
   const venReportMetadata = (oadrRegisterReport.reports || []).map(report => {
     const { reportSpecifierId, descriptions } = report;
-    const lastReceivedRegister = new Date().toISOString();
+
     const reportRequestId = v4();
     return {
-      reportRequestIds: [reportRequestId],
-      reportSpecifierId,
-      descriptions,
-      lastReceivedRegister,
+      report_request_ids: [reportRequestId],
+      report_specifier_id: reportSpecifierId,
+      descriptions: descriptions.map(description => {
+        return {
+          report_id: description.reportId,
+          report_type: description.reportType,
+          reading_type: description.readingType,
+          sampling_rate: {
+            min_period: description.samplingRate.minPeriod,
+            max_period: description.samplingRate.maxPeriod,
+            on_change: description.samplingRate.onChange,
+          },
+        };
+      }),
+      last_received_register: new Date().toISOString(),
     };
   });
 
   //TODO: whitelist based off Nantum API sensors
 
-  await nantum.updateReport(clientCertificateFingerprint, {
-    venReportMetadata,
+  await nantum.updateVen(ven._id, {
+    reports: venReportMetadata,
   });
 
   return {
@@ -172,28 +209,30 @@ async function createdReports(
   );
 
   validateVenId(oadrCreatedReport.venId, clientCertificateFingerprint, false);
+  const ven = await nantum.getVen(clientCertificateFingerprint);
+  if (!ven) {
+    throw new Error('VEN is not registered');
+  }
 
   if (oadrCreatedReport.pendingReports) {
     // flag reports as having been created
-    const report = await nantum.fetchReport(clientCertificateFingerprint);
-    if (report.venReportMetadata) {
+    if (ven.reports) {
       for (const pendingReport of oadrCreatedReport.pendingReports) {
         const reportRequestId = pendingReport['reportRequestId'];
-        const match = report.venReportMetadata.filter(x =>
-          x.reportRequestIds.includes(reportRequestId),
+        const match = ven.reports.filter(x =>
+          x.report_request_ids.includes(reportRequestId),
         )[0];
         if (match) {
-          match.lastReceivedCreated = new Date().toISOString();
+          match.last_received_created = new Date().toISOString();
         } else {
-          logger.info(
-            'could not match',
-            reportRequestId,
-            report.venReportMetadata,
-          );
+          logger.info('could not match', reportRequestId, ven.reports);
         }
       }
     }
-    await nantum.updateReport(clientCertificateFingerprint, report);
+
+    await nantum.updateVen(ven._id, {
+      reports: ven.reports,
+    });
   }
 
   return {
@@ -218,38 +257,44 @@ async function receiveReportData(
 
   const requestVenId = oadrUpdateReport.venId;
   validateVenId(requestVenId, clientCertificateFingerprint, false);
+  const ven = await nantum.getVen(clientCertificateFingerprint);
+  if (!ven) {
+    throw new Error('VEN is not registered');
+  }
 
-  const report = await nantum.fetchReport(clientCertificateFingerprint);
-  if (report.venReportMetadata) {
+  if (ven.reports) {
+    const readings = [];
     for (const updateReport of oadrUpdateReport.reports) {
       const reportRequestId = updateReport.reportRequestId;
-      const match = report.venReportMetadata.filter(x =>
-        x.reportRequestIds.includes(reportRequestId),
+      const match = ven.reports.filter(x =>
+        x.report_request_ids.includes(reportRequestId),
       )[0];
       if (!match) {
-        logger.info(
-          'could not match',
-          reportRequestId,
-          report.venReportMetadata,
-        );
+        logger.info('could not match', reportRequestId, ven.reports);
         continue;
       }
-      match.lastReceivedUpdate = new Date().toISOString();
+      match.last_received_update = new Date().toISOString();
       for (const interval of updateReport.intervals || []) {
         const reportId = interval.reportPayloads[0].reportId;
+        const reportDefinition = _.find(match.descriptions, {
+          report_id: reportId,
+        });
+        if (!reportDefinition) {
+          logger.info('Received data for unknown report ' + reportId);
+          return;
+        }
         const date = interval.startDate;
 
         if (interval.reportPayloads[0].payloadFloat) {
           const value = interval.reportPayloads[0].payloadFloat;
-          logger.info('received report', [
+          readings.push({
             date,
-            clientCertificateFingerprint,
-            updateReport.reportSpecifierId,
-            updateReport.reportName,
-            reportRequestId,
-            reportId,
+            report_specifier_id: updateReport.reportSpecifierId,
+            report_name: updateReport.reportName,
+            report_id: reportId,
+            report_type: reportDefinition.report_type,
             value,
-          ]);
+          });
         }
         if (
           interval.reportPayloads[0].payloadStatus &&
@@ -261,24 +306,28 @@ async function receiveReportData(
             const typeObj = loadControlState[type];
             Object.keys(typeObj).forEach(subType => {
               const value = typeObj[subType];
-              logger.info('received report', [
+              readings.push({
                 date,
-                clientCertificateFingerprint,
-                updateReport.reportSpecifierId,
-                updateReport.reportName,
-                reportRequestId,
-                reportId,
+                report_specifier_id: updateReport.reportSpecifierId,
+                report_name: updateReport.reportName,
+                report_id: reportId,
+                report_type: reportDefinition.report_type,
                 type,
-                subType,
+                sub_type: subType,
                 value,
-              ]);
+              });
             });
           });
         }
       }
     }
+    if (readings.length > 0) {
+      await nantum.sendReportReadings(ven, readings);
+    }
   }
-  await nantum.updateReport(clientCertificateFingerprint, report);
+  await nantum.updateVen(ven._id, {
+    reports: ven.reports,
+  });
 
   return {
     _type: 'oadrUpdatedReport',

+ 2 - 2
xml/event/created-event.js

@@ -85,8 +85,8 @@ function serializeEventResponse(eventResponse) {
   const descriptionFrag =
     eventResponse.responseDescription != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:responseDescription')
-        .txt(eventResponse.responseDescription)
+          .ele(energyInteropNs, 'ei:responseDescription')
+          .txt(eventResponse.responseDescription)
       : fragment();
 
   return fragment()

+ 56 - 50
xml/event/distribute-event.js

@@ -1,5 +1,7 @@
 'use strict';
 
+const _ = require('lodash');
+
 const {
   parseXML,
   childAttr,
@@ -37,65 +39,65 @@ const eiTargetMappings = [
     xmlNsUri: powerNs,
     xmlElement: 'endDeviceAsset',
     xmlChildElement: 'mrid',
-    json: 'endDeviceAsset',
+    json: 'end-device-asset',
   },
   {
     xmlNs: 'power',
     xmlNsUri: powerNs,
     xmlElement: 'aggregatedPnode',
     xmlChildElement: 'node',
-    json: 'aggregatedPnode',
+    json: 'aggregated-pnode',
   },
   {
     xmlNs: 'power',
     xmlNsUri: powerNs,
     xmlElement: 'meterAsset',
     xmlChildElement: 'mrid',
-    json: 'meterAsset',
+    json: 'meter-asset',
   },
   {
     xmlNs: 'power',
     xmlNsUri: powerNs,
     xmlElement: 'pnode',
     xmlChildElement: 'node',
-    json: 'pNode',
+    json: 'pnode',
   },
   {
     xmlNs: 'power',
     xmlNsUri: powerNs,
     xmlElement: 'serviceDeliveryPoint',
     xmlChildElement: 'node',
-    json: 'serviceDeliveryPoint',
+    json: 'service-delivery-point',
   },
   {
     xmlNs: 'ei',
     xmlNsUri: energyInteropNs,
     xmlElement: 'groupID',
-    json: 'groupId',
+    json: 'group',
   },
   {
     xmlNs: 'ei',
     xmlNsUri: energyInteropNs,
     xmlElement: 'groupName',
-    json: 'groupName',
+    json: 'group-name',
   },
   {
     xmlNs: 'ei',
     xmlNsUri: energyInteropNs,
     xmlElement: 'resourceID',
-    json: 'resourceId',
+    json: 'resource',
   },
   {
     xmlNs: 'ei',
     xmlNsUri: energyInteropNs,
     xmlElement: 'venID',
-    json: 'venId',
+    json: 'ven',
   },
   {
     xmlNs: 'ei',
     xmlNsUri: energyInteropNs,
     xmlElement: 'partyID',
-    json: 'partyId',
+    json: 'party',
   },
 ];
 
@@ -110,7 +112,7 @@ const itemBaseMappings = [
         json: 'powerAttributes',
       },
     ],
-    json: 'powerReal',
+    json: 'power-real',
   },
   {
     xmlNs: 'power',
@@ -122,7 +124,7 @@ const itemBaseMappings = [
         json: 'powerAttributes',
       },
     ],
-    json: 'powerReactive',
+    json: 'power-reactive',
   },
   {
     xmlNs: 'power',
@@ -134,7 +136,7 @@ const itemBaseMappings = [
         json: 'powerAttributes',
       },
     ],
-    json: 'powerApparent',
+    json: 'power-apparent',
   },
   {
     xmlNs: 'power',
@@ -146,37 +148,37 @@ const itemBaseMappings = [
     xmlNs: 'power',
     xmlNsUri: powerNs,
     xmlElement: 'energyApparent',
-    json: 'energyApparent',
+    json: 'energy-apparent',
   },
   {
     xmlNs: 'power',
     xmlNsUri: powerNs,
     xmlElement: 'energyReactive',
-    json: 'energyReactive',
+    json: 'energy-reactive',
   },
   {
     xmlNs: 'power',
     xmlNsUri: powerNs,
     xmlElement: 'energyReal',
-    json: 'energyReal',
+    json: 'energy-real',
   },
   {
     xmlNs: 'oadr',
     xmlNsUri: oadrNs,
     xmlElement: 'currencyPerKWh',
-    json: 'currencyPerKWh',
+    json: 'currency-per-kwh',
   },
   {
     xmlNs: 'oadr',
     xmlNsUri: oadrNs,
     xmlElement: 'currencyPerKW',
-    json: 'currencyPerKW',
+    json: 'currency-per-kw',
   },
   {
     xmlNs: 'oadr',
     xmlNsUri: oadrNs,
     xmlElement: 'currencyPerThm',
-    json: 'currencyPerThm',
+    json: 'currency-per-thm',
   },
   {
     xmlNs: 'oadr',
@@ -212,13 +214,13 @@ const itemBaseMappings = [
     xmlNs: 'oadr',
     xmlNsUri: oadrNs,
     xmlElement: 'pulseCount',
-    json: 'pulseCount',
+    json: 'pulse-count',
   },
   {
     xmlNs: 'oadr',
     xmlNsUri: oadrNs,
     xmlElement: 'customUnit',
-    json: 'customUnit',
+    json: 'custom-unit',
   },
 ];
 
@@ -406,7 +408,7 @@ function parseToleranceTolerateStartAfter(tolerance) {
 }
 
 function parseEiTarget(eiTarget) {
-  const result = {};
+  const result = [];
   for (const eiTargetMapping of eiTargetMappings) {
     const unNamespacedAttribute = eiTargetMapping.xmlElement;
     if (eiTarget[unNamespacedAttribute]) {
@@ -418,8 +420,13 @@ function parseEiTarget(eiTarget) {
       } else {
         newValues = eiTargetValue;
       }
-      const existing = result[eiTargetMapping.json] || [];
-      result[eiTargetMapping.json] = [...existing, ...newValues];
+      const newTargets = newValues.map(value => {
+        return {
+          type: eiTargetMapping.json,
+          value,
+        };
+      });
+      result.push(...newTargets);
     }
   }
   return result;
@@ -428,23 +435,23 @@ function parseEiTarget(eiTarget) {
 function serializeEiTarget(eiTarget) {
   const result = fragment();
   const targetElement = result.ele(energyInteropNs, 'ei:eiTarget');
+  const groupedByType = _.groupBy(eiTarget, item => item.type);
 
   for (const eiTargetMapping of eiTargetMappings) {
-    if (eiTarget[eiTargetMapping.json]) {
-      eiTarget[eiTargetMapping.json].forEach(target => {
-        const {
-          xmlNs,
-          xmlNsUri,
-          xmlElement,
-          xmlChildElement,
-        } = eiTargetMapping;
+    const { xmlNs, xmlNsUri, xmlElement, xmlChildElement } = eiTargetMapping;
+
+    const byType = groupedByType[eiTargetMapping.json];
+    if (byType) {
+      byType.forEach(target => {
         if (xmlChildElement) {
           targetElement
             .ele(xmlNsUri, `${xmlNs}:${xmlElement}`)
             .ele(xmlNsUri, `${xmlNs}:${xmlChildElement}`)
-            .txt(target);
+            .txt(target.value);
         } else {
-          targetElement.ele(xmlNsUri, `${xmlNs}:${xmlElement}`).txt(target);
+          targetElement
+            .ele(xmlNsUri, `${xmlNs}:${xmlElement}`)
+            .txt(target.value);
         }
       });
     }
@@ -467,7 +474,7 @@ function parseEventSignal(eventSignal) {
 
   const eiTarget = childAttr(eventSignal, 'eiTarget');
   if (eiTarget != null) {
-    result.target = parseEiTarget(eiTarget['$$']);
+    result.targets = parseEiTarget(eiTarget['$$']);
   }
 
   const currentValue = childAttr(eventSignal, 'currentValue');
@@ -498,8 +505,8 @@ function serializeEventSignal(eventSignal) {
     .ele(energyInteropNs, 'ei:signalType')
     .txt(eventSignal.signalType);
 
-  if (eventSignal.target) {
-    eiEventSignal.import(serializeEiTarget(eventSignal.target));
+  if (eventSignal.targets) {
+    eiEventSignal.import(serializeEiTarget(eventSignal.targets));
   }
 
   if (eventSignal.currentValue != null) {
@@ -588,7 +595,7 @@ function parseEiEventSignals(eiEventSignals) {
   }
 
   if (wrappedBaselines) {
-    result.baseline = wrappedBaselines.map(x => parseEventBaseline(x['$$']));
+    result.baseline = wrappedBaselines.map(x => parseEventBaseline(x['$$']))[0];
   }
 
   return result;
@@ -596,11 +603,10 @@ function parseEiEventSignals(eiEventSignals) {
 
 function serializeEiEventSignals(eiEventSignals) {
   const eventSignals = eiEventSignals.event.map(x => serializeEventSignal(x));
-  const eventBaselines = eiEventSignals.baseline
-    ? eiEventSignals.baseline.map(x => serializeEventBaseline(x))
-    : [];
-
-  return [...eventSignals, ...eventBaselines];
+  if (eiEventSignals.baseline) {
+    return [...eventSignals, serializeEventBaseline(eiEventSignals.baseline)];
+  }
+  return eventSignals;
 }
 
 function parseEiActivePeriod(activePeriod) {
@@ -710,7 +716,7 @@ function parseEiEvent(eiEvent) {
     eventDescriptor: parseEventDescriptor(eiEvent.eventDescriptor[0]['$$']),
     activePeriod: parseEiActivePeriod(eiEvent.eiActivePeriod[0]['$$']),
     signals: parseEiEventSignals(eiEvent.eiEventSignals[0]['$$']),
-    target: parseEiTarget(eiEvent.eiTarget[0]['$$']),
+    targets: parseEiTarget(eiEvent.eiTarget[0]['$$']),
   };
 }
 
@@ -731,8 +737,8 @@ function serializeEiEvent(eiEvent) {
     eiEventSignals.import(signal),
   );
 
-  if (eiEvent.target) {
-    eiEventResult.import(serializeEiTarget(eiEvent.target));
+  if (eiEvent.targets) {
+    eiEventResult.import(serializeEiTarget(eiEvent.targets));
   }
 
   return result;
@@ -818,15 +824,15 @@ function serialize(obj) {
   const vtnId =
     obj.vtnId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:vtnID')
-        .txt(obj.vtnId)
+          .ele(energyInteropNs, 'ei:vtnID')
+          .txt(obj.vtnId)
       : fragment();
 
   const requestId =
     obj.requestId != null
       ? fragment()
-        .ele(energyInteropPayloadsNs, 'pyld:requestID')
-        .txt(obj.requestId)
+          .ele(energyInteropPayloadsNs, 'pyld:requestID')
+          .txt(obj.requestId)
       : fragment();
 
   const doc = createDoc()

+ 2 - 2
xml/event/request-event.js

@@ -31,8 +31,8 @@ function serializeEiRequestEvent(requestId, venId, replyLimit) {
   const replyLimitFrag =
     replyLimit != null
       ? fragment()
-        .ele(energyInteropPayloadsNs, 'pyld:replyLimit')
-        .txt(replyLimit)
+          .ele(energyInteropPayloadsNs, 'pyld:replyLimit')
+          .txt(replyLimit)
       : fragment();
 
   return fragment()

+ 2 - 2
xml/poll/oadr-response.js

@@ -50,8 +50,8 @@ function serialize(obj) {
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
 
   const doc = createDoc()

+ 2 - 2
xml/register-party/cancel-party-registration.js

@@ -27,8 +27,8 @@ function serialize(obj) {
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
 
   const doc = createDoc()

+ 4 - 4
xml/register-party/canceled-party-registration.js

@@ -58,14 +58,14 @@ function serialize(obj) {
   const registrationId =
     obj.registrationId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:registrationID')
-        .txt(obj.registrationId)
+          .ele(energyInteropNs, 'ei:registrationID')
+          .txt(obj.registrationId)
       : fragment();
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
 
   const doc = createDoc()

+ 10 - 10
xml/register-party/create-party-registration.js

@@ -52,32 +52,32 @@ function serialize(obj) {
   const registrationId =
     obj.registrationId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:registrationID')
-        .txt(obj.registrationId)
+          .ele(energyInteropNs, 'ei:registrationID')
+          .txt(obj.registrationId)
       : fragment();
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
   const oadrTransportAddress =
     obj.oadrTransportAddress != null
       ? fragment()
-        .ele(oadrNs, 'oadr2b:oadrTransportAddress')
-        .txt(obj.oadrTransportAddress)
+          .ele(oadrNs, 'oadr2b:oadrTransportAddress')
+          .txt(obj.oadrTransportAddress)
       : fragment();
   const oadrVenName =
     obj.oadrVenName != null
       ? fragment()
-        .ele(oadrNs, 'oadr2b:oadrVenName')
-        .txt(obj.oadrVenName)
+          .ele(oadrNs, 'oadr2b:oadrVenName')
+          .txt(obj.oadrVenName)
       : fragment();
   const oadrHttpPullModel =
     obj.oadrHttpPullModel != null
       ? fragment()
-        .ele(oadrNs, 'oadr2b:oadrHttpPullModel')
-        .txt(obj.oadrHttpPullModel)
+          .ele(oadrNs, 'oadr2b:oadrHttpPullModel')
+          .txt(obj.oadrHttpPullModel)
       : fragment();
 
   const doc = createDoc()

+ 6 - 6
xml/register-party/created-party-registration.js

@@ -72,20 +72,20 @@ function serialize(obj) {
   const registrationId =
     obj.registrationId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:registrationID')
-        .txt(obj.registrationId)
+          .ele(energyInteropNs, 'ei:registrationID')
+          .txt(obj.registrationId)
       : fragment();
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
   const vtnId =
     obj.vtnId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:vtnID')
-        .txt(obj.vtnId)
+          .ele(energyInteropNs, 'ei:vtnID')
+          .txt(obj.vtnId)
       : fragment();
 
   const doc = createDoc()

+ 3 - 3
xml/report/created-report.js

@@ -37,7 +37,7 @@ async function parse(input) {
   const result = {
     _type: 'oadrCreatedReport',
     responseCode: code,
-    responseRequestId: requestId
+    responseRequestId: requestId,
   };
 
   if (description != null) result.responseDescription = description;
@@ -79,8 +79,8 @@ function serialize(obj) {
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
 
   const doc = createDoc()

+ 2 - 2
xml/report/register-report.js

@@ -32,8 +32,8 @@ function serialize(obj) {
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
 
   const doc = createDoc()

+ 2 - 2
xml/report/registered-report.js

@@ -61,8 +61,8 @@ function serialize(obj) {
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
 
   const doc = createDoc()

+ 2 - 2
xml/report/updated-report.js

@@ -39,8 +39,8 @@ function serialize(obj) {
   const venId =
     obj.venId != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:venID')
-        .txt(obj.venId)
+          .ele(energyInteropNs, 'ei:venID')
+          .txt(obj.venId)
       : fragment();
 
   const doc = createDoc()

+ 4 - 4
xml/shared.js

@@ -32,8 +32,8 @@ function serializeDateTime(dateTime) {
 function serializeDuration(duration) {
   return duration != null
     ? fragment()
-      .ele(calendarNs, 'cal:duration')
-      .txt(duration)
+        .ele(calendarNs, 'cal:duration')
+        .txt(duration)
     : fragment();
 }
 
@@ -458,8 +458,8 @@ function serializeEiResponse(data) {
   const descriptionFrag =
     data.responseDescription != null
       ? fragment()
-        .ele(energyInteropNs, 'ei:responseDescription')
-        .txt(data.responseDescription)
+          .ele(energyInteropNs, 'ei:responseDescription')
+          .txt(data.responseDescription)
       : fragment();
 
   return fragment()