Просмотр исходного кода

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 лет назад
Родитель
Сommit
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
 ## Environment
 Please set the following environment variables:
 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
 * `COMPANY`: Which company we're associated to
 * `NODE_ENV`: Which environment we're running in. Can be `production` | `development` | `test`.
 * `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`
 * `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
 * `PORT`: The TCP port the webserver should bind to
 * `REGION`: AWS region to use for Kinesis
 * `REGION`: AWS region to use for Kinesis
@@ -22,8 +23,8 @@ Please set the following environment variables:
 ## Running locally for development
 ## Running locally for development
 
 
 ### Environment
 ### 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
 ### 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
 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`.
 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
 ## Client certificate authentication
 
 
 OpenADR VENs connect using a client TLS certificate. In this Docker-compose configuration, nginx provides:
 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';
 'use strict';
 
 
-const { readFileSync } = require('fs');
-const path = require('path');
 const { expect } = require('chai');
 const { expect } = require('chai');
 const sinon = require('sinon');
 const sinon = require('sinon');
+const { pki } = require('node-forge');
+const {
+  calculatePartialFingerprintOfEscapedPemCertificate,
+} = require('../../modules/certificate');
 
 
 const { Ven } = require('../../client/ven');
 const { Ven } = require('../../client/ven');
 const app = require('../../server');
 const app = require('../../server');
-const { sequelize, Ven: VenDb } = require('../../db');
 const { port } = require('../../config');
 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() {
   describe('registration and event retrieval', async function() {
     let clock;
     let clock;
 
 
@@ -26,20 +77,8 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
       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 () => {
     it('should successfully return a vtnId from queryRegistration', async () => {
@@ -75,7 +114,7 @@ describe('VEN to VTN interactions', function() {
     after(async () => {
     after(async () => {
       await app.stop();
       await app.stop();
     });
     });
-  });
+  }).timeout(5000);
 
 
   describe('poll', async function() {
   describe('poll', async function() {
     let ven;
     let ven;
@@ -90,20 +129,8 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
       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();
       await ven.register();
     });
     });
 
 
@@ -113,10 +140,15 @@ describe('VEN to VTN interactions', function() {
       expect(pollResponse.events.length).to.eql(1);
       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 () => {
     after(async () => {
       await app.stop();
       await app.stop();
     });
     });
-  });
+  }).timeout(5000);
 
 
   describe('report', async function() {
   describe('report', async function() {
     let ven;
     let ven;
@@ -131,27 +163,10 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
       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();
       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 () => {
     it('should successfully subscribe to reports and receive data', async () => {
@@ -164,7 +179,7 @@ describe('VEN to VTN interactions', function() {
           reportName: 'METADATA_TELEMETRY_STATUS',
           reportName: 'METADATA_TELEMETRY_STATUS',
           descriptions: [
           descriptions: [
             {
             {
-              reportId: 'ts1',
+              reportId: 'TelemetryStatusReport',
               reportType: 'x-resourceStatus',
               reportType: 'x-resourceStatus',
               readingType: 'x-notApplicable',
               readingType: 'x-notApplicable',
               samplingRate: {
               samplingRate: {
@@ -183,7 +198,7 @@ describe('VEN to VTN interactions', function() {
           reportName: 'METADATA_TELEMETRY_USAGE',
           reportName: 'METADATA_TELEMETRY_USAGE',
           descriptions: [
           descriptions: [
             {
             {
-              reportId: 'rep1',
+              reportId: 'TelemetryUsageReport',
               reportType: 'usage',
               reportType: 'usage',
               readingType: 'Direct Read',
               readingType: 'Direct Read',
               samplingRate: {
               samplingRate: {
@@ -230,7 +245,7 @@ describe('VEN to VTN interactions', function() {
                 {
                 {
                   dataQuality: 'Quality Good - Non Specific',
                   dataQuality: 'Quality Good - Non Specific',
                   payloadFloat: 161.97970171999845,
                   payloadFloat: 161.97970171999845,
-                  reportId: 'rep1',
+                  reportId: 'TelemetryUsageReport',
                 },
                 },
               ],
               ],
               startDate: '2020-05-08T21:26:49.562-06:00',
               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',
               startDate: '2020-05-13T10:56:11.058-06:00',
@@ -274,12 +289,12 @@ describe('VEN to VTN interactions', function() {
       ];
       ];
 
 
       await ven.sendReportData(reports);
       await ven.sendReportData(reports);
-    });
+    }).timeout(10000);
 
 
     after(async () => {
     after(async () => {
       await app.stop();
       await app.stop();
     });
     });
-  });
+  }).timeout(5000);
 
 
   describe('optIn', async function() {
   describe('optIn', async function() {
     let clock;
     let clock;
@@ -294,47 +309,27 @@ describe('VEN to VTN interactions', function() {
       clock = sinon.useFakeTimers(
       clock = sinon.useFakeTimers(
         new Date('2020-04-26T01:00:00.000Z').getTime(),
         new Date('2020-04-26T01:00:00.000Z').getTime(),
       );
       );
-      await sequelize.sync();
-      await VenDb.destroy({ truncate: true });
       await app.start();
       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();
       await ven.register();
       pollResponse = await ven.poll();
       pollResponse = await ven.poll();
       events = pollResponse.events;
       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 eventId = events[0].eventDescriptor.eventId;
       const modificationNumber = events[0].eventDescriptor.modificationNumber;
       const modificationNumber = events[0].eventDescriptor.modificationNumber;
       await ven.opt('optIn', eventId, 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 () => {
     after(async () => {
       await app.stop();
       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';
 'use strict';
 
 
 const sampleEvent1 = {
 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 = {
 module.exports = {
   sampleEvent1,
   sampleEvent1,
   sampleReport1,
   sampleReport1,
+  sampleVen1,
 };
 };

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

@@ -5,21 +5,21 @@ const { v4 } = require('uuid');
 const sinon = require('sinon');
 const sinon = require('sinon');
 const rewire = require('rewire');
 const rewire = require('rewire');
 
 
+const FakeNantumModule = require('../utils/fake-nantum-module');
+
 const {
 const {
   requestEventMax,
   requestEventMax,
-  createdEventMax,
   createdEventInvalidEventId1,
   createdEventInvalidEventId1,
 } = require('../xml/event/js-requests');
 } = require('../xml/event/js-requests');
 
 
 const { poll } = require('../xml/poll/js-requests');
 const { poll } = require('../xml/poll/js-requests');
 
 
 const { generatedFromNantumEvent1 } = require('../xml/event/js-responses');
 const { generatedFromNantumEvent1 } = require('../xml/event/js-responses');
-const { sampleEvent1 } = require('../modules/nantum-responses');
+const { sampleEvent1, sampleVen1 } = require('../modules/nantum-responses');
 
 
 describe('Event', function() {
 describe('Event', function() {
   let clock;
   let clock;
-  let sandbox, rewired;
-  let fetchEventStub;
+  let rewired;
 
 
   after(async () => {
   after(async () => {
     clock.restore();
     clock.restore();
@@ -27,25 +27,15 @@ describe('Event', function() {
 
 
   before(async () => {
   before(async () => {
     clock = sinon.useFakeTimers(new Date('2020-04-26T01:00:00.000Z').getTime());
     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 = rewire('../../../processes/event.js');
     rewired.__set__({
     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 venId = poll.venId;
       const commonName = v4()
       const commonName = v4()
         .replace(/-/g, '')
         .replace(/-/g, '')
@@ -96,8 +67,6 @@ describe('Event', function() {
         venId,
         venId,
       );
       );
       expect(pollResponse1.events.length).to.eql(1);
       expect(pollResponse1.events.length).to.eql(1);
-
-      await rewired.updateOptType(createdEventMax, commonName, venId);
       const pollResponse2 = await rewired.pollForEvents(
       const pollResponse2 = await rewired.pollForEvents(
         poll,
         poll,
         commonName,
         commonName,

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

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

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

@@ -5,6 +5,9 @@ const { v4 } = require('uuid');
 const sinon = require('sinon');
 const sinon = require('sinon');
 const rewire = require('rewire');
 const rewire = require('rewire');
 
 
+const FakeNantumModule = require('../utils/fake-nantum-module');
+const { sampleEvent1, sampleVen1 } = require('../modules/nantum-responses');
+
 const {
 const {
   registerReportMax,
   registerReportMax,
   createdReportGenerated1,
   createdReportGenerated1,
@@ -23,7 +26,7 @@ const { sampleReport1 } = require('../modules/nantum-responses');
 describe('Report', function() {
 describe('Report', function() {
   let clock;
   let clock;
   let rewired;
   let rewired;
-  let report;
+  let nantum;
   let uuidSequence;
   let uuidSequence;
 
 
   after(async () => {
   after(async () => {
@@ -36,16 +39,14 @@ describe('Report', function() {
   });
   });
 
 
   before(async () => {
   before(async () => {
-    report = {};
+    nantum = new FakeNantumModule({
+      events: [sampleEvent1],
+      vens: [sampleVen1],
+    });
 
 
     rewired = rewire('../../../processes/report.js');
     rewired = rewire('../../../processes/report.js');
     rewired.__set__({
     rewired.__set__({
-      nantum: {
-        fetchReport: venId => Promise.resolve(report[venId] || []),
-        updateReport: async (venId, newReport) => {
-          report[venId] = newReport;
-        },
-      },
+      nantum,
       v4: () => `uuid${uuidSequence++}`,
       v4: () => `uuid${uuidSequence++}`,
     });
     });
   });
   });
@@ -63,7 +64,8 @@ describe('Report', function() {
         venId,
         venId,
       );
       );
       expect(registeredReport.responseCode).to.eql('200');
       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 () => {
     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',
       responseCode: '200',
       responseRequestId: '336f7e47b92eefe985ec',
       responseRequestId: '336f7e47b92eefe985ec',
       optType: 'optIn',
       optType: 'optIn',
-      eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
-      modificationNumber: 2,
+      eventId: '5f0649904d122ec115231f69',
+      modificationNumber: 0,
     },
     },
   ],
   ],
 };
 };
@@ -49,8 +49,8 @@ const createdEventMax = {
       responseDescription: 'OK',
       responseDescription: 'OK',
       responseRequestId: '336f7e47b92eefe985ec',
       responseRequestId: '336f7e47b92eefe985ec',
       optType: 'optIn',
       optType: 'optIn',
-      eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
-      modificationNumber: 2,
+      eventId: '5f0649904d122ec115231f69',
+      modificationNumber: 0,
     },
     },
   ],
   ],
 };
 };
@@ -85,7 +85,7 @@ const createdEventOldModificationNumber1 = {
       responseDescription: 'OK',
       responseDescription: 'OK',
       responseRequestId: '336f7e47b92eefe985ec',
       responseRequestId: '336f7e47b92eefe985ec',
       optType: 'optIn',
       optType: 'optIn',
-      eventId: 'a2fa542eca8d4e829ff5c0f0c8e68710',
+      eventId: '5f0649904d122ec115231f69',
       modificationNumber: 0,
       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',
       responseRequired: 'always',
     },
     },
   ],
   ],
@@ -107,9 +110,12 @@ const distributeEventMax = {
             signalName: 'LOAD_CONTROL',
             signalName: 'LOAD_CONTROL',
             signalType: 'x-loadControlCapacity',
             signalType: 'x-loadControlCapacity',
             signalId: '64ba02508ab099d6eae6',
             signalId: '64ba02508ab099d6eae6',
-            target: {
-              endDeviceAsset: ['Energy_Management_System'],
-            },
+            targets: [
+              {
+                type: 'end-device-asset',
+                value: 'Energy_Management_System',
+              },
+            ],
             currentValue: 0,
             currentValue: 0,
           },
           },
           {
           {
@@ -126,37 +132,41 @@ const distributeEventMax = {
             currentValue: 0,
             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',
       responseRequired: 'always',
     },
     },
     {
     {
@@ -187,7 +197,7 @@ const distributeEventMax = {
             signalId: '38e550909d77bc37310d',
             signalId: '38e550909d77bc37310d',
             currentValue: 0,
             currentValue: 0,
             itemBase: {
             itemBase: {
-              type: 'powerReal',
+              type: 'power-real',
               description: 'RealPower',
               description: 'RealPower',
               units: 'W',
               units: 'W',
               siScaleCode: 'none',
               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',
       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',
       responseRequired: 'always',
     },
     },
   ],
   ],
@@ -319,9 +335,12 @@ const distributeEventEpri1 = {
             signalName: 'LOAD_CONTROL',
             signalName: 'LOAD_CONTROL',
             signalType: 'x-loadControlCapacity',
             signalType: 'x-loadControlCapacity',
             signalId: '64ba02508ab099d6eae6',
             signalId: '64ba02508ab099d6eae6',
-            target: {
-              endDeviceAsset: ['Energy_Management_System'],
-            },
+            targets: [
+              {
+                type: 'end-device-asset',
+                value: 'Energy_Management_System',
+              },
+            ],
             currentValue: 0,
             currentValue: 0,
           },
           },
           {
           {
@@ -338,37 +357,41 @@ const distributeEventEpri1 = {
             currentValue: 0,
             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',
       responseRequired: 'always',
     },
     },
     {
     {
@@ -399,7 +422,7 @@ const distributeEventEpri1 = {
             signalId: '38e550909d77bc37310d',
             signalId: '38e550909d77bc37310d',
             currentValue: 0,
             currentValue: 0,
             itemBase: {
             itemBase: {
-              type: 'powerReal',
+              type: 'power-real',
               description: 'RealPower',
               description: 'RealPower',
               units: 'W',
               units: 'W',
               siScaleCode: 'none',
               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',
       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',
       responseRequired: 'always',
     },
     },
   ],
   ],
 };
 };
 
 
 const generatedFromNantumEvent1 = {
 const generatedFromNantumEvent1 = {
+  _type: 'oadrDistributeEvent',
   responseCode: '200',
   responseCode: '200',
   responseDescription: 'OK',
   responseDescription: 'OK',
   responseRequestId: '2233',
   responseRequestId: '2233',
@@ -484,43 +514,84 @@ const generatedFromNantumEvent1 = {
   events: [
   events: [
     {
     {
       eventDescriptor: {
       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,
         testEvent: false,
         priority: 0,
         priority: 0,
       },
       },
       activePeriod: {
       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: {
       signals: {
         event: [
         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: [
             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',
       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>
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <ei:optType>optIn</ei:optType>
       <ei:optType>optIn</ei:optType>
       <ei:qualifiedEventID>
       <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:qualifiedEventID>
      </ei:eventResponse>
      </ei:eventResponse>
     </ei:eventResponses>
     </ei:eventResponses>
@@ -90,8 +90,8 @@ const createdEventMaxXml = `<oadr2b:oadrPayload xmlns:oadr2b="http://openadr.org
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <ei:optType>optIn</ei:optType>
       <ei:optType>optIn</ei:optType>
       <ei:qualifiedEventID>
       <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:qualifiedEventID>
      </ei:eventResponse>
      </ei:eventResponse>
     </ei:eventResponses>
     </ei:eventResponses>
@@ -115,7 +115,7 @@ const createdEventMissingRequiredXml = `<oadr2b:oadrPayload xmlns:oadr2b="http:/
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <pyld:requestID>336f7e47b92eefe985ec</pyld:requestID>
       <ei:optType>optIn</ei:optType>
       <ei:optType>optIn</ei:optType>
       <ei:qualifiedEventID>
       <ei:qualifiedEventID>
-       <ei:eventID>a2fa542eca8d4e829ff5c0f0c8e68710</ei:eventID>
+       <ei:eventID>5f0649904d122ec115231f69</ei:eventID>
       </ei:qualifiedEventID>
       </ei:qualifiedEventID>
      </ei:eventResponse>
      </ei:eventResponse>
     </ei:eventResponses>
     </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,
   createdReportMinXml,
   createdReportMaxXml,
   createdReportMaxXml,
   createdReportMissingRequiredXml,
   createdReportMissingRequiredXml,
-  createdReportErrorXml
+  createdReportErrorXml,
 } = require('./xml-requests');
 } = require('./xml-requests');
 const { createdReportMin, createdReportMax } = require('./js-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',
       reportName: 'METADATA_TELEMETRY_STATUS',
       descriptions: [
       descriptions: [
         {
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           reportType: 'x-resourceStatus',
           reportType: 'x-resourceStatus',
           readingType: 'x-notApplicable',
           readingType: 'x-notApplicable',
           samplingRate: {
           samplingRate: {
@@ -73,7 +73,7 @@ const registerReportCa = {
       reportName: 'METADATA_TELEMETRY_STATUS',
       reportName: 'METADATA_TELEMETRY_STATUS',
       descriptions: [
       descriptions: [
         {
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           reportType: 'x-resourceStatus',
           reportType: 'x-resourceStatus',
           readingType: 'x-notApplicable',
           readingType: 'x-notApplicable',
           samplingRate: {
           samplingRate: {

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

@@ -39,7 +39,7 @@ const registeredReportMax = {
       duration: 'PT24H',
       duration: 'PT24H',
       specifiers: [
       specifiers: [
         {
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
           readingType: 'x-notApplicable',
         },
         },
       ],
       ],
@@ -84,7 +84,7 @@ const createReportMax = {
       duration: 'PT24H',
       duration: 'PT24H',
       specifiers: [
       specifiers: [
         {
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
           readingType: 'x-notApplicable',
         },
         },
       ],
       ],
@@ -105,7 +105,7 @@ const createReportGenerated1 = {
       duration: 'PT3600S',
       duration: 'PT3600S',
       specifiers: [
       specifiers: [
         {
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
           readingType: 'x-notApplicable',
         },
         },
       ],
       ],
@@ -144,7 +144,7 @@ const createReportGenerated2 = {
       duration: 'PT3600S',
       duration: 'PT3600S',
       specifiers: [
       specifiers: [
         {
         {
-          reportId: 'ts1',
+          reportId: 'TelemetryStatusReport',
           readingType: 'x-notApplicable',
           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>PT1H</xcal:duration>
     </xcal:duration>
     </xcal:duration>
     <oadr2b:oadrReportDescription>
     <oadr2b:oadrReportDescription>
-     <ei:rID>ts1</ei:rID>
+     <ei:rID>TelemetryStatusReport</ei:rID>
      <ei:reportType>x-resourceStatus</ei:reportType>
      <ei:reportType>x-resourceStatus</ei:reportType>
      <ei:readingType>x-notApplicable</ei:readingType>
      <ei:readingType>x-notApplicable</ei:readingType>
      <oadr2b:oadrSamplingRate>
      <oadr2b:oadrSamplingRate>
@@ -130,7 +130,7 @@ const registerReportMissingRequiredXml = `<oadr2b:oadrPayload xmlns:oadr2b="http
      <xcal:duration>PT1H</xcal:duration>
      <xcal:duration>PT1H</xcal:duration>
     </xcal:duration>
     </xcal:duration>
     <oadr2b:oadrReportDescription>
     <oadr2b:oadrReportDescription>
-     <ei:rID>ts1</ei:rID>
+     <ei:rID>TelemetryStatusReport</ei:rID>
      <ei:reportType>x-resourceStatus</ei:reportType>
      <ei:reportType>x-resourceStatus</ei:reportType>
      <ei:readingType>x-notApplicable</ei:readingType>
      <ei:readingType>x-notApplicable</ei:readingType>
      <oadr2b:oadrSamplingRate>
      <oadr2b:oadrSamplingRate>
@@ -178,7 +178,7 @@ const registerReportCaXml = `<oadr2b:oadrPayload xmlns:oadr2b="http://openadr.or
 <xcal:duration>PT1H</xcal:duration>
 <xcal:duration>PT1H</xcal:duration>
 </xcal:duration>
 </xcal:duration>
 <oadr2b:oadrReportDescription>
 <oadr2b:oadrReportDescription>
-<ei:rID>ts1</ei:rID>
+<ei:rID>TelemetryStatusReport</ei:rID>
 <ei:reportType>x-resourceStatus</ei:reportType>
 <ei:reportType>x-resourceStatus</ei:reportType>
 <ei:readingType>x-notApplicable</ei:readingType>
 <ei:readingType>x-notApplicable</ei:readingType>
 <oadr2b:oadrSamplingRate>
 <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>
       </ns5:properties>
      </ns3:reportInterval>
      </ns3:reportInterval>
      <ns3:specifierPayload>
      <ns3:specifierPayload>
-      <ns3:rID>ts1</ns3:rID>
+      <ns3:rID>TelemetryStatusReport</ns3:rID>
       <ns3:readingType>x-notApplicable</ns3:readingType>
       <ns3:readingType>x-notApplicable</ns3:readingType>
      </ns3:specifierPayload>
      </ns3:specifierPayload>
     </ns3:reportSpecifier>
     </ns3:reportSpecifier>
@@ -96,7 +96,7 @@ const createReportMissingRequiredXml = `<ns2:oadrPayload xmlns="http://www.w3.or
       </ns5:properties>
       </ns5:properties>
      </ns3:reportInterval>
      </ns3:reportInterval>
      <ns3:specifierPayload>
      <ns3:specifierPayload>
-      <ns3:rID>ts1</ns3:rID>
+      <ns3:rID>TelemetryStatusReport</ns3:rID>
       <ns3:readingType>x-notApplicable</ns3:readingType>
       <ns3:readingType>x-notApplicable</ns3:readingType>
      </ns3:specifierPayload>
      </ns3:specifierPayload>
     </ns3:reportSpecifier>
     </ns3:reportSpecifier>
@@ -164,7 +164,7 @@ const registeredReportMaxXml = `<ns2:oadrPayload xmlns="http://www.w3.org/2000/0
       </ns5:properties>
       </ns5:properties>
      </ns3:reportInterval>
      </ns3:reportInterval>
      <ns3:specifierPayload>
      <ns3:specifierPayload>
-      <ns3:rID>ts1</ns3:rID>
+      <ns3:rID>TelemetryStatusReport</ns3:rID>
       <ns3:readingType>x-notApplicable</ns3:readingType>
       <ns3:readingType>x-notApplicable</ns3:readingType>
      </ns3:specifierPayload>
      </ns3:specifierPayload>
     </ns3:reportSpecifier>
     </ns3:reportSpecifier>

+ 3 - 4
config/development.js

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

+ 2 - 3
config/production.js

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

+ 2 - 3
config/testing.js

@@ -1,6 +1,8 @@
 'use strict';
 'use strict';
 
 
 module.exports = {
 module.exports = {
+  company: process.env.COMPANY || 'test_company',
+  nantumUrl: process.env.NANTUM_URL,
   port: 3002,
   port: 3002,
   loggerOptions: {
   loggerOptions: {
     cache: {
     cache: {
@@ -11,8 +13,5 @@ module.exports = {
       company: process.env.COMPANY || 'company_not_provided',
       company: process.env.COMPANY || 'company_not_provided',
     },
     },
   },
   },
-  dbConfig: {
-    uri: process.env.DB_URL,
-  },
   vtnId: 'TEST_VTN',
   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
       - nodejs
     restart: unless-stopped
     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:
   nodejs:
     container_name: nantum-vtn-nodejs
     container_name: nantum-vtn-nodejs
     build: .
     build: .
@@ -39,7 +24,6 @@ services:
     restart: on-failure
     restart: on-failure
     environment:
     environment:
       NODE_ENV: development
       NODE_ENV: development
-      DB_URL: postgres://vtn:vtn@nantum-vtn-db:5432/vtn_test
       NO_AWS: 'true'
       NO_AWS: 'true'
       PORT: 8080
       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
 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';
 'use strict';
 
 
 const logger = require('./logger');
 const logger = require('./logger');
-const { sequelize } = require('./db');
 
 
 logger.info('Starting OADR VTN...');
 logger.info('Starting OADR VTN...');
 
 
 (async () => {
 (async () => {
   try {
   try {
-    await sequelize.sync();
     await require('./server').start();
     await require('./server').start();
     logger.info('OADR VTN up and running');
     logger.info('OADR VTN up and running');
   } catch (e) {
   } catch (e) {

+ 79 - 92
modules/nantum.js

@@ -1,120 +1,107 @@
 'use strict';
 '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 = {
 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",
     "start": "node index.js",
     "test": "npm run unit",
     "test": "npm run unit",
     "unit": "NODE_ENV=test _mocha --exit $(find __tests__ -name \"*.spec.js\")",
     "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": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "git+https://cdemoll@bitbucket.org/HWPD/cloud_oadr-vtn.git"
     "url": "git+https://cdemoll@bitbucket.org/HWPD/cloud_oadr-vtn.git"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@hw/edge-sdks": "^2.0.17",
+    "@hw/kinesis-logger": "^1.0.3",
     "axios": "^0.19.2",
     "axios": "^0.19.2",
     "bluebird": "^3.7.2",
     "bluebird": "^3.7.2",
     "body-parser": "^1.19.0",
     "body-parser": "^1.19.0",
     "eslint": "^5.16.0",
     "eslint": "^5.16.0",
     "express": "^4.16.4",
     "express": "^4.16.4",
+    "lodash": "^4.17.15",
     "node-forge": "^0.9.1",
     "node-forge": "^0.9.1",
-    "pg": "^7.18.2",
-    "sequelize": "^5.21.6",
     "uuid": "^7.0.3",
     "uuid": "^7.0.3",
     "xml2js": "^0.4.23",
     "xml2js": "^0.4.23",
-    "xmlbuilder2": "^2.1.1",
-    "@hw/kinesis-logger": "^1.0.3"
+    "xmlbuilder2": "^2.1.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "chai": "^4.2.0",
     "chai": "^4.2.0",

+ 190 - 130
processes/event.js

@@ -4,122 +4,142 @@ const logger = require('../logger');
 const nantum = require('../modules/nantum');
 const nantum = require('../modules/nantum');
 const { vtnId } = require('../config');
 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();
   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 {
     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(
 async function retrieveEvents(
@@ -155,15 +175,22 @@ async function retrieveEvents(
     throw error;
     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 {
   return {
+    _type: 'oadrDistributeEvent',
     responseCode: '200',
     responseCode: '200',
     responseDescription: 'OK',
     responseDescription: 'OK',
     responseRequestId: oadrRequestEvent.requestId || '',
     responseRequestId: oadrRequestEvent.requestId || '',
     requestId: oadrRequestEvent.requestId || '',
     requestId: oadrRequestEvent.requestId || '',
     vtnId: vtnId,
     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(
   const staleResponses = eventResponses.filter(
     eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents),
     eventResponse => !eventResponseMatchesValidEvent(eventResponse, oadrEvents),
   );
   );
@@ -211,24 +238,29 @@ async function updateOptType(
   const requestVenId = oadrCreatedEvent.venId;
   const requestVenId = oadrCreatedEvent.venId;
   validateVenId(requestVenId, clientCertificateFingerprint, true);
   validateVenId(requestVenId, clientCertificateFingerprint, true);
 
 
-  let opted = await nantum.fetchOpted(requestVenId);
+  const ven = await nantum.getVen(clientCertificateFingerprint);
 
 
   try {
   try {
-    await validateEventResponses(requestVenId, oadrCreatedEvent.eventResponses);
+    await validateEventResponses(ven, oadrCreatedEvent.eventResponses);
     for (const eventResponse of 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 {
     return {
       _type: 'oadrResponse',
       _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(
   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;
   const requestVenId = oadrPoll.venId;
   validateVenId(requestVenId, clientCertificateFingerprint, true);
   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 {
     return {
       _type: 'oadrDistributeEvent',
       _type: 'oadrDistributeEvent',
       responseCode: '200',
       responseCode: '200',
@@ -288,7 +348,7 @@ async function pollForEvents(
       responseRequestId: '', // required field, but empty is allowed as per spec
       responseRequestId: '', // required field, but empty is allowed as per spec
       requestId: '',
       requestId: '',
       vtnId: vtnId,
       vtnId: vtnId,
-      events: filteredEvents,
+      events,
     };
     };
   }
   }
   return undefined;
   return undefined;

+ 41 - 41
processes/registration.js

@@ -21,32 +21,40 @@ async function registerParty(
   validateVenId(requestVenId, clientCertificateFingerprint, true);
   validateVenId(requestVenId, clientCertificateFingerprint, true);
   validateCreatePartyRegistration(oadrCreatePartyRegistration);
   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');
       const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       error.responseCode = 452;
       throw error;
       throw error;
     }
     }
-    if (nantumRegistration.registration_id == null) {
+    if (ven.registration_id == null) {
       const registrationId = v4().replace(/-/g, '');
       const registrationId = v4().replace(/-/g, '');
-      nantumRegistration.registration_id = registrationId;
-      await nantum.updateRegistration(nantumRegistration);
+      await nantum.updateVen(ven._id, {
+        registration_id: registrationId,
+      });
     }
     }
   } else {
   } else {
     const registrationId = v4().replace(/-/g, '');
     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,
       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,
     oadrCreatePartyRegistration.requestId,
-    nantumRegistration,
+    ven,
   );
   );
 }
 }
 
 
@@ -105,23 +113,20 @@ async function query(
 
 
   const requestVenId = clientCertificateFingerprint;
   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');
       const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       error.responseCode = 452;
       throw error;
       throw error;
     }
     }
   } else {
   } else {
     // response payload should not contain ven_id or registration_id
     // 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(
 async function cancelParty(
@@ -140,30 +145,28 @@ async function cancelParty(
   validateVenId(requestVenId, clientCertificateFingerprint, false);
   validateVenId(requestVenId, clientCertificateFingerprint, false);
   const venId = clientCertificateFingerprint;
   const venId = clientCertificateFingerprint;
 
 
-  let nantumRegistration = await nantum.fetchRegistration(requestVenId);
+  let ven = await nantum.getVen(requestVenId);
+
   let cancelledRegistrationId;
   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');
       const error = new Error('Client certificate CN mismatch');
       error.responseCode = 452;
       error.responseCode = 452;
       throw error;
       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
     // 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 {
   return {
@@ -175,16 +178,13 @@ async function cancelParty(
   };
   };
 }
 }
 
 
-function nantumRegistrationToOadrRegistrationCreated(
-  requestId,
-  nantumRegistration,
-) {
+function venToOadrRegistrationCreated(requestId, ven) {
   return {
   return {
     responseRequestId: requestId || '',
     responseRequestId: requestId || '',
     responseCode: '200',
     responseCode: '200',
     responseDescription: 'OK',
     responseDescription: 'OK',
-    registrationId: nantumRegistration.registration_id,
-    venId: nantumRegistration.ven_id,
+    registrationId: ven.registration_id,
+    venId: ven.client_certificate_fingerprint,
     vtnId: vtnId,
     vtnId: vtnId,
     pollFreqDuration: 'PT10S',
     pollFreqDuration: 'PT10S',
   };
   };

+ 115 - 66
processes/report.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 4 - 4
xml/shared.js

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