Selaa lähdekoodia

PROD-2283: EiReport implementation

 * Controllers receive oadrRegisterReport, oadrCreatedReport, oadrUpdateReport messages
 * OadrPoll sends oadrCreateReport to subscribe to every registered report
Blake Schneider 5 vuotta sitten
vanhempi
commit
e2502bcaaf
48 muutettua tiedostoa jossa 3645 lisäystä ja 568 poistoa
  1. 2 1
      README.md
  2. 168 3
      __tests__/integration/end-to-end.spec.js
  3. 52 0
      __tests__/unit/modules/nantum-responses.js
  4. 228 0
      __tests__/unit/processes/report.spec.js
  5. 1 1
      __tests__/unit/xml/event/distribute-event.spec.js
  6. 0 2
      __tests__/unit/xml/event/js-responses.js
  7. 50 0
      __tests__/unit/xml/report/create-report.spec.js
  8. 61 0
      __tests__/unit/xml/report/created-report.spec.js
  9. 265 0
      __tests__/unit/xml/report/js-requests.js
  10. 196 0
      __tests__/unit/xml/report/js-responses.js
  11. 61 0
      __tests__/unit/xml/report/register-report.spec.js
  12. 53 0
      __tests__/unit/xml/report/registered-report.spec.js
  13. 67 0
      __tests__/unit/xml/report/update-report.spec.js
  14. 50 0
      __tests__/unit/xml/report/updated-report.spec.js
  15. 377 0
      __tests__/unit/xml/report/xml-requests.js
  16. 242 0
      __tests__/unit/xml/report/xml-responses.js
  17. 51 76
      client/ven.js
  18. 0 0
      docker_build.sh
  19. 0 0
      docker_run_psql.sh
  20. 0 0
      docker_run_tests.sh
  21. 31 6
      modules/nantum.js
  22. 3 0
      processes/event.js
  23. 314 0
      processes/report.js
  24. 12 12
      server/controllers/poll.js
  25. 63 0
      server/controllers/report.js
  26. 2 0
      server/middleware/xml-parser.js
  27. 1 0
      server/routes/index.js
  28. 10 0
      server/routes/report.js
  29. 15 43
      xml/event/created-event.js
  30. 39 176
      xml/event/distribute-event.js
  31. 12 15
      xml/event/request-event.js
  32. 11 0
      xml/index.js
  33. 1 1
      xml/parser.js
  34. 7 12
      xml/poll/oadr-poll.js
  35. 15 56
      xml/poll/oadr-response.js
  36. 8 17
      xml/register-party/cancel-party-registration.js
  37. 15 50
      xml/register-party/canceled-party-registration.js
  38. 8 17
      xml/register-party/create-party-registration.js
  39. 18 65
      xml/register-party/created-party-registration.js
  40. 7 15
      xml/register-party/query-registration.js
  41. 66 0
      xml/report/create-report.js
  42. 112 0
      xml/report/created-report.js
  43. 10 0
      xml/report/index.js
  44. 68 0
      xml/report/register-report.js
  45. 95 0
      xml/report/registered-report.js
  46. 62 0
      xml/report/update-report.js
  47. 71 0
      xml/report/updated-report.js
  48. 645 0
      xml/shared.js

+ 2 - 1
README.md

@@ -131,4 +131,5 @@ ExpressJS middleware.
 * [ ] Not implemented
 
 ### EiReport service
-* [ ] Not implemented
+* [x] `oadrRegisterReport`, `oadrRegisteredReport`, `oadrCreateReport`, `oadrCreatedReport`, `oadrUpdateReport`, `oadrUpdatedReport` messages
+* [ ] `oadrCancelReport`, `oadrCanceledReport`

+ 168 - 3
__tests__/integration/end-to-end.spec.js

@@ -11,6 +11,8 @@ const { sequelize, Ven: VenDb } = require('../../db');
 const { port } = require('../../config');
 
 describe('VEN to VTN interactions', function() {
+  const venId = '17:32:59:FD:0E:B5:99:31:27:9C';
+
   describe('registration and event retrieval', async function() {
     let clock;
 
@@ -35,7 +37,7 @@ describe('VEN to VTN interactions', function() {
         `http://127.0.0.1:${port}`,
         clientCrtPem,
         'aabbccddeeff',
-        '17:32:59:FD:0E:B5:99:31:27:9C',
+        venId,
         'ven.js1',
       );
     });
@@ -99,7 +101,7 @@ describe('VEN to VTN interactions', function() {
         `http://127.0.0.1:${port}`,
         clientCrtPem,
         'aabbccddeeff',
-        '17:32:59:FD:0E:B5:99:31:27:9C',
+        venId,
         'ven.js1',
       );
       await ven.register();
@@ -116,6 +118,169 @@ describe('VEN to VTN interactions', function() {
     });
   });
 
+  describe('report', async function() {
+    let ven;
+
+    let clock;
+
+    after(async () => {
+      clock.restore();
+    });
+
+    before(async () => {
+      clock = sinon.useFakeTimers(
+        new Date('2020-04-26T01:00:00.000Z').getTime(),
+      );
+      await sequelize.sync();
+      await VenDb.destroy({ truncate: true });
+      await app.start();
+      const clientCrtPem = readFileSync(
+        path.join(__dirname, 'integration-client.crt'),
+        'utf-8',
+      );
+      ven = new Ven(
+        `http://127.0.0.1:${port}`,
+        clientCrtPem,
+        'aabbccddeeff',
+        venId,
+        'ven.js1',
+      );
+      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);
+    });
+
+    it('should successfully subscribe to reports and receive data', async () => {
+      const reqs = [
+        {
+          reportRequestId: '31c5ce71a68a73ece370',
+          reportSpecifierId: 'TELEMETRY_STATUS',
+          createdDateTime: '2020-05-07T10:05:41.421-06:00',
+          duration: 'PT1H',
+          reportName: 'METADATA_TELEMETRY_STATUS',
+          descriptions: [
+            {
+              reportId: 'ts1',
+              reportType: 'x-resourceStatus',
+              readingType: 'x-notApplicable',
+              samplingRate: {
+                minPeriod: 'PT1M',
+                maxPeriod: 'PT1H',
+                onChange: false,
+              },
+            },
+          ],
+        },
+        {
+          reportRequestId: '3d92d98e0b65d94e60a7',
+          reportSpecifierId: 'TELEMETRY_USAGE',
+          createdDateTime: '2020-05-07T10:05:41.421-06:00',
+          duration: 'PT1H',
+          reportName: 'METADATA_TELEMETRY_USAGE',
+          descriptions: [
+            {
+              reportId: 'rep1',
+              reportType: 'usage',
+              readingType: 'Direct Read',
+              samplingRate: {
+                minPeriod: 'PT1M',
+                maxPeriod: 'PT1H',
+                onChange: false,
+              },
+            },
+          ],
+        },
+      ];
+      // register reports
+      await ven.registerReports(reqs);
+      const pollResponse = await ven.poll();
+
+      // poll has a request to create reports
+      expect(pollResponse._type).to.eql('oadrCreateReport');
+
+      const createRequestId = pollResponse.reportRequestId;
+      const [
+        telemetryStatusReportRequestId,
+        telemetryUsageReportRequestId,
+      ] = pollResponse.requests.map(x => x.reportRequestId);
+
+      // notify vtn that reports have been created
+      await ven.notifyCreatedReports(createRequestId, [
+        telemetryStatusReportRequestId,
+        telemetryUsageReportRequestId,
+      ]);
+
+      // poll is empty, no reports to create
+      const pollResponse2 = await ven.poll();
+      expect(pollResponse2._type).to.eql('oadrResponse');
+
+      // send report
+      const reports = [
+        {
+          createdDateTime: '2020-05-08T21:27:49.591-06:00',
+          duration: 'PT1M',
+          intervals: [
+            {
+              duration: 'PT1M',
+              reportPayloads: [
+                {
+                  dataQuality: 'Quality Good - Non Specific',
+                  payloadFloat: 161.97970171999845,
+                  reportId: 'rep1',
+                },
+              ],
+              startDate: '2020-05-08T21:26:49.562-06:00',
+            },
+          ],
+          reportName: 'TELEMETRY_USAGE',
+          reportRequestId: telemetryUsageReportRequestId,
+          reportSpecifierId: 'TELEMETRY_USAGE',
+          startDate: '2020-05-08T21:26:49.562-06:00',
+        },
+        {
+          createdDateTime: '2020-05-13T10:56:11.058-06:00',
+          duration: 'PT1M',
+          intervals: [
+            {
+              duration: 'PT1M',
+              reportPayloads: [
+                {
+                  dataQuality: 'Quality Good - Non Specific',
+                  payloadStatus: {
+                    online: true,
+                    manualOverride: false,
+                    loadControlState: {
+                      oadrLevelOffset: {
+                        oadrNormal: 40,
+                        oadrCurrent: 50,
+                      },
+                    },
+                  },
+                  reportId: 'rep1',
+                },
+              ],
+              startDate: '2020-05-13T10:56:11.058-06:00',
+            },
+          ],
+          reportName: 'TELEMETRY_STATUS',
+          reportRequestId: telemetryStatusReportRequestId,
+          reportSpecifierId: 'TELEMETRY_STATUS',
+          startDate: '2020-05-13T10:56:11.058-06:00',
+        },
+      ];
+
+      await ven.sendReportData(reports);
+    });
+
+    after(async () => {
+      await app.stop();
+    });
+  });
+
   describe('optIn', async function() {
     let clock;
 
@@ -140,7 +305,7 @@ describe('VEN to VTN interactions', function() {
         `http://127.0.0.1:${port}`,
         clientCrtPem,
         'aabbccddeeff',
-        '17:32:59:FD:0E:B5:99:31:27:9C',
+        venId,
         'ven.js1',
       );
       await ven.register();

+ 52 - 0
__tests__/unit/modules/nantum-responses.js

@@ -27,6 +27,58 @@ const sampleEvent1 = {
   },
 };
 
+const sampleReport1 = {
+  'D8:1D:4B:20:5A:65:4C:50:32:FA': {
+    venReportMetadata: [
+      {
+        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',
+      },
+      {
+        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',
+      },
+    ],
+  },
+};
+
 module.exports = {
   sampleEvent1,
+  sampleReport1,
 };

+ 228 - 0
__tests__/unit/processes/report.spec.js

@@ -0,0 +1,228 @@
+'use strict';
+
+const { expect } = require('chai');
+const { v4 } = require('uuid');
+const sinon = require('sinon');
+const rewire = require('rewire');
+
+const {
+  registerReportMax,
+  createdReportGenerated1,
+  updateReportTelemetryStatus,
+} = require('../xml/report/js-requests');
+
+const {
+  createReportGenerated1,
+  createReportGenerated2,
+} = require('../xml/report/js-responses');
+
+const { poll: oadrPollMessage } = require('../xml/poll/js-requests');
+
+const { sampleReport1 } = require('../modules/nantum-responses');
+
+describe('Report', function() {
+  let clock;
+  let rewired;
+  let report;
+  let uuidSequence;
+
+  after(async () => {
+    clock.restore();
+  });
+
+  beforeEach(async () => {
+    clock = sinon.useFakeTimers(new Date('2020-04-26T01:00:00.000Z').getTime());
+    uuidSequence = 0;
+  });
+
+  before(async () => {
+    report = {};
+
+    rewired = rewire('../../../processes/report.js');
+    rewired.__set__({
+      nantum: {
+        fetchReport: venId => Promise.resolve(report[venId] || []),
+        updateReport: async (venId, newReport) => {
+          report[venId] = newReport;
+        },
+      },
+      v4: () => `uuid${uuidSequence++}`,
+    });
+  });
+
+  describe('requesting VEN reports', function() {
+    it('successfully registers reports', async () => {
+      const venId = registerReportMax.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      const registeredReport = await rewired.registerReports(
+        registerReportMax,
+        commonName,
+        venId,
+      );
+      expect(registeredReport.responseCode).to.eql('200');
+      expect(report).to.eql(sampleReport1);
+    });
+
+    it('requests reports on next poll', async () => {
+      const venId = registerReportMax.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      const pollResponse = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse).to.eql(createReportGenerated1);
+    });
+
+    it('does not request reports again immediately following poll', async () => {
+      const venId = registerReportMax.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      const pollResponse = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse).to.eql(undefined);
+    });
+
+    it('does request reports again immediately following poll when enough time has elapsed', async () => {
+      clock = sinon.useFakeTimers(
+        new Date('2020-04-26T01:01:30.000Z').getTime(),
+      );
+      const venId = registerReportMax.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      const pollResponse = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse).to.eql(createReportGenerated2);
+    });
+
+    it('re-sends create request when no data received for 95 seconds', async () => {
+      const venId = registerReportMax.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      const pollResponse1 = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse1).to.eql(undefined);
+
+      await rewired.registerReports(registerReportMax, commonName, venId);
+
+      const pollResponse2 = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      // should immediately request 2 reports
+      expect(pollResponse2).to.not.be.undefined;
+      expect(pollResponse2.requests.length).to.eql(2);
+
+      // ven should respond that it has accepted those 2 report requests
+      const createdResponse = await rewired.createdReports(
+        createdReportGenerated1,
+        commonName,
+        venId,
+      );
+      expect(createdResponse.responseCode).to.eql('200');
+
+      // ven sends report telemetry data
+      const receiveResponse = await rewired.receiveReportData(
+        updateReportTelemetryStatus,
+        commonName,
+        venId,
+      );
+      expect(receiveResponse.responseCode).to.eql('200');
+
+      // advance time 45 seconds. subscription is valid, should not resubscribe
+      clock = sinon.useFakeTimers(
+        new Date('2020-04-26T01:00:45.000Z').getTime(),
+      );
+      const pollResponse3 = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse3).to.be.undefined;
+
+      clock = sinon.useFakeTimers(
+        new Date('2020-04-26T01:01:35.000Z').getTime(),
+      ); // advance +95 seconds
+      // should trigger a create request because data is stale
+      const pollResponse4 = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse4).to.not.be.undefined;
+      expect(pollResponse4._type).to.eql('oadrCreateReport');
+      expect(pollResponse4.requests.length).to.eql(2);
+    });
+
+    it('re-sends create request when created not received', async () => {
+      const venId = registerReportMax.venId;
+      const commonName = v4()
+        .replace(/-/g, '')
+        .substring(0, 12);
+
+      await rewired.registerReports(registerReportMax, commonName, venId);
+
+      const pollResponse1 = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse1).to.not.be.undefined;
+      expect(pollResponse1.requests.length).to.eql(2);
+
+      // advance time 8 seconds. should offer to re-subscribe
+      clock = sinon.useFakeTimers(
+        new Date('2020-04-26T01:00:08.000Z').getTime(),
+      );
+
+      const pollResponse2 = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse2).to.not.be.undefined;
+      expect(pollResponse2.requests.length).to.eql(2);
+
+      // now VEN sends created response
+      const createdResponse = await rewired.createdReports(
+        createdReportGenerated1,
+        commonName,
+        venId,
+      );
+      expect(createdResponse.responseCode).to.eql('200');
+
+      // advance time 8 seconds. should not offer to re-subscribe.
+      clock = sinon.useFakeTimers(
+        new Date('2020-04-26T01:00:16.000Z').getTime(),
+      );
+      const pollResponse3 = await rewired.pollForReports(
+        oadrPollMessage,
+        commonName,
+        venId,
+      );
+      expect(pollResponse3).to.be.undefined;
+    });
+  });
+});

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
__tests__/unit/xml/event/distribute-event.spec.js


+ 0 - 2
__tests__/unit/xml/event/js-responses.js

@@ -182,7 +182,6 @@ const distributeEventMax = {
       signals: {
         event: [
           {
-            intervals: [],
             signalName: 'BID_LOAD',
             signalType: 'level',
             signalId: '38e550909d77bc37310d',
@@ -395,7 +394,6 @@ const distributeEventEpri1 = {
       signals: {
         event: [
           {
-            intervals: [],
             signalName: 'BID_LOAD',
             signalType: 'level',
             signalId: '38e550909d77bc37310d',

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 50 - 0
__tests__/unit/xml/report/create-report.spec.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 61 - 0
__tests__/unit/xml/report/created-report.spec.js


+ 265 - 0
__tests__/unit/xml/report/js-requests.js

@@ -0,0 +1,265 @@
+'use strict';
+
+const registerReportMin = {
+  _type: 'oadrRegisterReport',
+  requestId: '31c5ce71a68a73ece370',
+  reports: [],
+};
+
+const registerReportMax = {
+  _type: 'oadrRegisterReport',
+  requestId: '31c5ce71a68a73ece370',
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+  reports: [
+    {
+      reportRequestId: '31c5ce71a68a73ece370',
+      reportSpecifierId: 'TELEMETRY_STATUS',
+      createdDateTime: '2020-05-07T10:05:41.421-06:00',
+      duration: 'PT1H',
+      reportName: 'METADATA_TELEMETRY_STATUS',
+      descriptions: [
+        {
+          reportId: 'ts1',
+          reportType: 'x-resourceStatus',
+          readingType: 'x-notApplicable',
+          samplingRate: {
+            minPeriod: 'PT1M',
+            maxPeriod: 'PT1H',
+            onChange: false,
+          },
+        },
+      ],
+    },
+    {
+      reportRequestId: '3d92d98e0b65d94e60a7',
+      reportSpecifierId: 'TELEMETRY_USAGE',
+      createdDateTime: '2020-05-07T10:05:41.421-06:00',
+      duration: 'PT1H',
+      reportName: 'METADATA_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,
+          },
+        },
+      ],
+    },
+  ],
+};
+
+const registerReportCa = {
+  _type: 'oadrRegisterReport',
+  requestId: '1087c',
+  reports: [
+    {
+      reportRequestId: '1087c',
+      reportSpecifierId: 'TELEMETRY_STATUS',
+      createdDateTime: '2020-05-11T19:32:33.632-06:00',
+      reportName: 'METADATA_TELEMETRY_STATUS',
+      descriptions: [
+        {
+          reportId: 'ts1',
+          reportType: 'x-resourceStatus',
+          readingType: 'x-notApplicable',
+          samplingRate: {
+            minPeriod: 'PT1M',
+            maxPeriod: 'PT1H',
+            onChange: false,
+          },
+        },
+      ],
+      duration: 'PT1H',
+    },
+    {
+      reportRequestId: '1087c',
+      reportSpecifierId: 'TELEMETRY_USAGE',
+      createdDateTime: '2020-05-11T19:32:33.632-06:00',
+      reportName: 'METADATA_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,
+          },
+        },
+      ],
+      duration: 'PT1H',
+    },
+  ],
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+};
+
+const createdReportMin = {
+  _type: 'oadrCreatedReport',
+  responseCode: '200',
+  responseRequestId: 'c6e8147ead4eb3ad3dff',
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+  pendingReports: [
+    {
+      reportRequestId: 'eec71ed4dfe1e52d65a5',
+    },
+  ],
+};
+
+const createdReportMax = {
+  _type: 'oadrCreatedReport',
+  responseCode: '200',
+  responseDescription: 'OK',
+  responseRequestId: 'c6e8147ead4eb3ad3dff',
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+  pendingReports: [
+    {
+      reportRequestId: 'eec71ed4dfe1e52d65a5',
+    },
+  ],
+};
+
+const createdReportGenerated1 = {
+  _type: 'oadrCreatedReport',
+  responseCode: '200',
+  responseDescription: 'OK',
+  responseRequestId: 'c6e8147ead4eb3ad3dff',
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+  pendingReports: [
+    {
+      reportRequestId: 'uuid0',
+    },
+    {
+      reportRequestId: 'uuid1',
+    },
+  ],
+};
+
+const updateReportMin = {
+  _type: 'oadrUpdateReport',
+  reports: [],
+  requestId: '87bbc1d44d903f317758',
+};
+
+const updateReportTelemetryStatus = {
+  _type: 'oadrUpdateReport',
+  reports: [
+    {
+      createdDateTime: '2020-05-13T10:56:11.058-06:00',
+      duration: 'PT1M',
+      intervals: [
+        {
+          duration: 'PT1M',
+          reportPayloads: [
+            {
+              dataQuality: 'Quality Good - Non Specific',
+              payloadStatus: {
+                online: true,
+                manualOverride: false,
+                loadControlState: {
+                  oadrLevelOffset: {
+                    oadrNormal: 40,
+                    oadrCurrent: 50,
+                  },
+                },
+              },
+              reportId: 'TelemetryStatusReport',
+            },
+          ],
+          startDate: '2020-05-13T10:56:11.058-06:00',
+        },
+      ],
+      reportName: 'TELEMETRY_STATUS',
+      reportRequestId: 'uuid0',
+      reportSpecifierId: 'TELEMETRY_STATUS',
+      startDate: '2020-05-13T10:56:11.058-06:00',
+    },
+  ],
+  requestId: 'uuid0',
+};
+
+const updateReportMax = {
+  _type: 'oadrUpdateReport',
+  reports: [
+    {
+      createdDateTime: '2020-05-08T21:27:49.591-06:00',
+      duration: 'PT1M',
+      intervals: [
+        {
+          duration: 'PT30S',
+          reportPayloads: [
+            {
+              dataQuality: 'Quality Good - Non Specific',
+              payloadFloat: 161.97970171999845,
+              reportId: 'rep1',
+            },
+          ],
+          startDate: '2020-05-08T21:26:49.562-06:00',
+        },
+        {
+          duration: 'PT30S',
+          reportPayloads: [
+            {
+              dataQuality: 'Quality Good - Non Specific',
+              payloadFloat: 165.46849970752885,
+              reportId: 'rep1',
+            },
+          ],
+          startDate: '2020-05-08T21:27:19.594-06:00',
+        },
+        {
+          duration: 'PT30S',
+          reportPayloads: [
+            {
+              dataQuality: 'Quality Good - Non Specific',
+              payloadFloat: 162.30118577122087,
+              reportId: 'rep1',
+            },
+          ],
+          startDate: '2020-05-08T21:27:49.591-06:00',
+        },
+      ],
+      reportName: 'TELEMETRY_USAGE',
+      reportRequestId: '87bbc1d44d903f317758',
+      reportSpecifierId: 'TELEMETRY_USAGE',
+      startDate: '2020-05-08T21:26:49.562-06:00',
+    },
+  ],
+  requestId: '87bbc1d44d903f317758',
+};
+
+module.exports = {
+  createdReportMin,
+  createdReportMax,
+  createdReportGenerated1,
+  registerReportMin,
+  registerReportMax,
+  registerReportCa,
+  updateReportMin,
+  updateReportMax,
+  updateReportTelemetryStatus,
+};

+ 196 - 0
__tests__/unit/xml/report/js-responses.js

@@ -0,0 +1,196 @@
+'use strict';
+
+const registeredReportMin = {
+  _type: 'oadrRegisteredReport',
+  responseCode: '200',
+  responseRequestId: '31c5ce71a68a73ece370',
+};
+
+const registeredReportMax = {
+  _type: 'oadrRegisteredReport',
+  responseCode: '200',
+  responseDescription: 'OK',
+  responseRequestId: '31c5ce71a68a73ece370',
+  requests: [
+    {
+      reportRequestId: '87bbc1d44d903f317758',
+      reportSpecifierId: 'TELEMETRY_USAGE',
+      granularityDuration: 'PT30S',
+      reportBackDuration: 'PT1M',
+      startDate: '2020-05-09T03:24:48.000Z',
+      duration: 'PT24H',
+      specifiers: [
+        {
+          reportId: 'rep1',
+          readingType: 'x-notApplicable',
+        },
+        {
+          reportId: 'rep2',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+    {
+      reportRequestId: '3d92d98e0b65d94e60a7',
+      reportSpecifierId: 'TELEMETRY_STATUS',
+      granularityDuration: 'PT30S',
+      reportBackDuration: 'PT2M',
+      startDate: '2020-05-09T03:25:44.000Z',
+      duration: 'PT24H',
+      specifiers: [
+        {
+          reportId: 'ts1',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+  ],
+};
+
+const createReportMin = {
+  _type: 'oadrCreateReport',
+  requests: [],
+  requestId: '4323',
+};
+
+const createReportMax = {
+  _type: 'oadrCreateReport',
+  requestId: '31c5ce71a68a73ece370',
+  requests: [
+    {
+      reportRequestId: '87bbc1d44d903f317758',
+      reportSpecifierId: 'TELEMETRY_USAGE',
+      granularityDuration: 'PT30S',
+      reportBackDuration: 'PT1M',
+      startDate: '2020-05-09T03:24:48.000Z',
+      duration: 'PT24H',
+      specifiers: [
+        {
+          reportId: 'rep1',
+          readingType: 'x-notApplicable',
+        },
+        {
+          reportId: 'rep2',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+    {
+      reportRequestId: '3d92d98e0b65d94e60a7',
+      reportSpecifierId: 'TELEMETRY_STATUS',
+      granularityDuration: 'PT30S',
+      reportBackDuration: 'PT2M',
+      startDate: '2020-05-09T03:25:44.000Z',
+      duration: 'PT24H',
+      specifiers: [
+        {
+          reportId: 'ts1',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+  ],
+};
+
+const createReportGenerated1 = {
+  _type: 'oadrCreateReport',
+  requestId: 'uuid2',
+  requests: [
+    {
+      reportRequestId: 'uuid0',
+      reportSpecifierId: 'TELEMETRY_STATUS',
+      granularityDuration: 'PT60S',
+      reportBackDuration: 'PT60S',
+      startDate: '2020-04-26T01:00:00.000Z',
+      duration: 'PT3600S',
+      specifiers: [
+        {
+          reportId: 'ts1',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+    {
+      reportRequestId: 'uuid1',
+      reportSpecifierId: 'TELEMETRY_USAGE',
+      granularityDuration: 'PT60S',
+      reportBackDuration: 'PT60S',
+      startDate: '2020-04-26T01:00:00.000Z',
+      duration: 'PT3600S',
+      specifiers: [
+        {
+          reportId: 'rep1',
+          readingType: 'x-notApplicable',
+        },
+        {
+          reportId: 'rep2',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+  ],
+};
+
+const createReportGenerated2 = {
+  _type: 'oadrCreateReport',
+  requestId: 'uuid2',
+  requests: [
+    {
+      reportRequestId: 'uuid0',
+      reportSpecifierId: 'TELEMETRY_STATUS',
+      granularityDuration: 'PT60S',
+      reportBackDuration: 'PT60S',
+      startDate: '2020-04-26T01:01:30.000Z',
+      duration: 'PT3600S',
+      specifiers: [
+        {
+          reportId: 'ts1',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+    {
+      reportRequestId: 'uuid1',
+      reportSpecifierId: 'TELEMETRY_USAGE',
+      granularityDuration: 'PT60S',
+      reportBackDuration: 'PT60S',
+      startDate: '2020-04-26T01:01:30.000Z',
+      duration: 'PT3600S',
+      specifiers: [
+        {
+          reportId: 'rep1',
+          readingType: 'x-notApplicable',
+        },
+        {
+          reportId: 'rep2',
+          readingType: 'x-notApplicable',
+        },
+      ],
+    },
+  ],
+};
+
+const updatedReportMin = {
+  _type: 'oadrUpdatedReport',
+  responseCode: '200',
+  responseRequestId: '4323',
+};
+
+const updatedReportMax = {
+  _type: 'oadrUpdatedReport',
+  responseCode: '200',
+  responseDescription: 'OK',
+  responseRequestId: '4323',
+  venId: 'D8:1D:4B:20:5A:65:4C:50:32:FA',
+};
+
+module.exports = {
+  createReportMax,
+  createReportMin,
+  createReportGenerated1,
+  createReportGenerated2,
+  registeredReportMax,
+  registeredReportMin,
+  updatedReportMin,
+  updatedReportMax,
+};

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 61 - 0
__tests__/unit/xml/report/register-report.spec.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 53 - 0
__tests__/unit/xml/report/registered-report.spec.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 67 - 0
__tests__/unit/xml/report/update-report.spec.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 50 - 0
__tests__/unit/xml/report/updated-report.spec.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 377 - 0
__tests__/unit/xml/report/xml-requests.js


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 242 - 0
__tests__/unit/xml/report/xml-responses.js


+ 51 - 76
client/ven.js

@@ -1,44 +1,9 @@
 'use strict';
 
-const {
-  serialize: serializeCreatePartyRegistration,
-} = require('../xml/register-party/create-party-registration');
-
-const {
-  serialize: serializeQueryRegistration,
-} = require('../xml/register-party/query-registration');
-
-const {
-  serialize: serializeCancelPartyRegistration,
-} = require('../xml/register-party/cancel-party-registration');
-
-const {
-  serialize: serializeRequestEvent,
-} = require('../xml/event/request-event');
-
-const { serialize: serializeOadrPoll } = require('../xml/poll/oadr-poll');
-
-const {
-  serialize: serializeCreatedEvent,
-} = require('../xml/event/created-event');
-
-const {
-  parse: parseCreatedPartyRegistration,
-} = require('../xml/register-party/created-party-registration');
-
-const {
-  parse: parseCanceledPartyRegistration,
-} = require('../xml/register-party/canceled-party-registration');
-
-const {
-  parse: parseDistributeEvent,
-} = require('../xml/event/distribute-event');
-
-const { parse: parsePollResponse } = require('../xml');
-
-const { parse: parseOadrResponse } = require('../xml/poll/oadr-response');
+const { parse, serialize } = require('../xml');
 
 const axios = require('axios');
+const { v4 } = require('uuid');
 const { escape } = require('querystring');
 
 class Ven {
@@ -61,15 +26,11 @@ class Ven {
 
   async queryRegistration() {
     const message = {
-      requestId: '2233',
+      _type: 'oadrQueryRegistration',
+      requestId: v4(),
     };
 
-    const createdResponse = await this.makeRequest(
-      'EiRegisterParty',
-      message,
-      serializeQueryRegistration,
-      parseCreatedPartyRegistration,
-    );
+    const createdResponse = await this.makeRequest('EiRegisterParty', message);
 
     // track registrationId for subsequent requests
     this.registrationId = createdResponse.registrationId;
@@ -78,7 +39,8 @@ class Ven {
 
   async cancelRegistration() {
     const message = {
-      requestId: '2233',
+      _type: 'oadrCancelPartyRegistration',
+      requestId: v4(),
       registrationId: this.registrationId,
       venId: this.venId,
     };
@@ -86,8 +48,6 @@ class Ven {
     const cancelledResponse = await this.makeRequest(
       'EiRegisterParty',
       message,
-      serializeCancelPartyRegistration,
-      parseCanceledPartyRegistration,
     );
 
     // track registrationId for subsequent requests
@@ -97,7 +57,8 @@ class Ven {
 
   async register() {
     const message = {
-      requestId: '2233',
+      _type: 'oadrCreatePartyRegistration',
+      requestId: v4(),
       registrationId: this.registrationId,
       venId: this.venId,
       oadrProfileName: '2.0b',
@@ -108,12 +69,7 @@ class Ven {
       oadrHttpPullModel: true,
     };
 
-    const createdResponse = await this.makeRequest(
-      'EiRegisterParty',
-      message,
-      serializeCreatePartyRegistration,
-      parseCreatedPartyRegistration,
-    );
+    const createdResponse = await this.makeRequest('EiRegisterParty', message);
 
     // track registrationId for subsequent requests
     this.registrationId = createdResponse.registrationId;
@@ -122,37 +78,30 @@ class Ven {
 
   async requestEvents() {
     const message = {
-      requestId: '2233',
+      _type: 'oadrRequestEvent',
+      requestId: v4(),
       venId: this.venId,
     };
 
-    const response = await this.makeRequest(
-      'EiEvent',
-      message,
-      serializeRequestEvent,
-      parseDistributeEvent,
-    );
+    const response = await this.makeRequest('EiEvent', message);
 
     return response;
   }
 
   async poll() {
     const message = {
+      _type: 'oadrPoll',
       venId: this.venId,
     };
 
-    const response = await this.makeRequest(
-      'OadrPoll',
-      message,
-      serializeOadrPoll,
-      parsePollResponse,
-    );
+    const response = await this.makeRequest('OadrPoll', message);
 
     return response;
   }
 
   async opt(optType, eventId, modificationNumber) {
     const message = {
+      _type: 'oadrCreatedEvent',
       responseCode: '200',
       responseDescription: 'OK',
       responseRequestId: '',
@@ -168,16 +117,42 @@ class Ven {
         },
       ],
     };
-    return await this.makeRequest(
-      'EiEvent',
-      message,
-      serializeCreatedEvent,
-      parseOadrResponse,
-    );
+    return await this.makeRequest('EiEvent', message);
+  }
+
+  async registerReports(oadrReports) {
+    const message = {
+      _type: 'oadrRegisterReport',
+      requestId: v4(),
+      venId: this.venId,
+      reports: oadrReports,
+    };
+    return await this.makeRequest('EiReport', message);
+  }
+
+  async notifyCreatedReports(createRequestId, reportRequestIds) {
+    const message = {
+      _type: 'oadrCreatedReport',
+      responseCode: '200',
+      responseDescription: 'OK',
+      responseRequestId: createRequestId,
+      venId: this.venId,
+      pendingReports: reportRequestIds.map(x => ({ reportRequestId: x })),
+    };
+    return await this.makeRequest('EiReport', message);
+  }
+
+  async sendReportData(reports) {
+    const message = {
+      _type: 'oadrUpdateReport',
+      reports: reports,
+      requestId: v4(),
+    };
+    return await this.makeRequest('EiReport', message);
   }
 
-  async makeRequest(service, message, serializer, parser) {
-    const xml = serializer(message);
+  async makeRequest(service, message) {
+    const xml = serialize(message);
 
     const config = {
       headers: {
@@ -194,7 +169,7 @@ class Ven {
       xml,
       config,
     );
-    const response = await parser(httpResponse.data);
+    const response = await parse(httpResponse.data);
 
     // but OpenADR provides its own response code in the XML envelope, we need to check that
     if (response.responseCode < 200 || response.responseCode >= 300) {

+ 0 - 0
docker_build.sh


+ 0 - 0
docker_run_psql.sh


+ 0 - 0
docker_run_tests.sh


+ 31 - 6
modules/nantum.js

@@ -4,19 +4,19 @@ const { Ven } = require('../db');
 
 async function fetchEvent(venId) {
   return {
-    event_identifier: '112233eca8d4e829ff5c0f0c8e68710',
+    event_identifier: 'aa2233eca8d4e829ff5c0f0c8e68710',
     client_id: venId,
     test_event: false,
-    event_mod_number: 2,
+    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-04-22T00:00:00.000Z',
-      start_time: '2020-04-22T06:00:00.000Z',
-      end_time: '2020-04-22T06:55:00.000Z',
+      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',
@@ -38,11 +38,19 @@ async function fetchRegistration(venId) {
   if (venRecord) return venRecord.data.registration;
 }
 
+async function fetchReport(venId) {
+  const venRecord = await Ven.findOne({
+    where: { ven_id: venId },
+  });
+  if (venRecord && venRecord.data.report) return venRecord.data.report;
+  return {};
+}
+
 async function fetchOpted(venId) {
   const venRecord = await Ven.findOne({
     where: { ven_id: venId },
   });
-  if (venRecord) return venRecord.data.opted || [];
+  if (venRecord && venRecord.data.opted) return venRecord.data.opted;
   return [];
 }
 
@@ -86,10 +94,27 @@ async function updateOpted(venId, opted) {
   await venRecord.save();
 }
 
+async function updateReport(venId, report) {
+  let venRecord = await Ven.findOne({
+    where: { ven_id: venId },
+  });
+
+  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();
+}
+
 module.exports = {
   fetchEvent,
   fetchOpted,
   fetchRegistration,
+  fetchReport,
   updateRegistration,
+  updateReport,
   updateOpted,
 };

+ 3 - 0
processes/event.js

@@ -231,12 +231,14 @@ async function updateOptType(
     await nantum.updateOpted(requestVenId, opted);
 
     return {
+      _type: 'oadrResponse',
       responseCode: '200',
       responseDescription: 'OK',
       venId: clientCertificateFingerprint,
     };
   } catch (e) {
     return {
+      _type: 'oadrResponse',
       responseCode: e.responseCode || '454',
       responseDescription: e.message || 'Invalid event response received',
       venId: clientCertificateFingerprint,
@@ -280,6 +282,7 @@ async function pollForEvents(
 
   if (filteredEvents.length > 0) {
     return {
+      _type: 'oadrDistributeEvent',
       responseCode: '200',
       responseDescription: 'OK',
       responseRequestId: '', // required field, but empty is allowed as per spec

+ 314 - 0
processes/report.js

@@ -0,0 +1,314 @@
+'use strict';
+
+const logger = require('../logger');
+const nantum = require('../modules/nantum');
+const { v4 } = require('uuid');
+
+const reportSubscriptionParameters = {
+  dataGranularitySeconds: 60,
+  reportBackSeconds: 60,
+  subscriptionDurationSeconds: 60 * 60,
+  resubscribeDurationSeconds: 59 * 60,
+  resubscribeAfterNoDataForSeconds: 90
+};
+
+function getSecondsSince(property, reportMetadata) {
+  const value = reportMetadata[property];
+  if (value != null) {
+    const millisDiff = new Date().getTime() - new Date(value).getTime();
+    if (millisDiff > 0) {
+      return Math.round(millisDiff / 1000);
+    }
+  }
+}
+
+async function pollForReports(
+  oadrPoll,
+  clientCertificateCn,
+  clientCertificateFingerprint,
+) {
+  logger.info(
+    'pollForReports',
+    oadrPoll,
+    clientCertificateCn,
+    clientCertificateFingerprint,
+  );
+
+  const report = await nantum.fetchReport(clientCertificateFingerprint);
+  const createRequests = [];
+
+  if (report.venReportMetadata) {
+    for (const reportMetadata of report.venReportMetadata) {
+      let sendCreate = false;
+
+      if (!reportMetadata.lastSentCreate) {
+        // if we've never sent a subscription request, do it
+        logger.info('sending create because we never have', reportMetadata.reportSpecifierId);
+        sendCreate = true;
+      } else {
+        // have sent a create > 5s ago, not received a created
+        if (
+          !reportMetadata.lastReceivedCreated &&
+          getSecondsSince('lastSentCreate', reportMetadata) > 5
+        ) {
+          logger.info('no reply to creation request, send another', reportMetadata.reportSpecifierId);
+          sendCreate = true;
+        }
+      }
+
+      if (
+        getSecondsSince('lastReceivedUpdate', reportMetadata) >
+        reportSubscriptionParameters.resubscribeAfterNoDataForSeconds
+      ) {
+        // previously received data, silent now
+        sendCreate = true;
+      }
+
+      if (
+        !reportMetadata.lastReceivedUpdate &&
+        getSecondsSince('lastReceivedCreated', reportMetadata) >
+          reportSubscriptionParameters.reportBackSeconds + 5
+      ) {
+        // if we haven't received any data but we've waited long enough for one data interval + 5 seconds
+        logger.info('sending create because have not received data', reportMetadata.reportSpecifierId);
+        sendCreate = true;
+      }
+
+      if (
+        getSecondsSince('lastReceivedCreated', reportMetadata) >
+        reportSubscriptionParameters.resubscribeDurationSeconds
+      ) {
+        // when we're close to the end of the subscription, trigger a resubscribe
+        logger.info('sending create because close to end of subscription', reportMetadata.reportSpecifierId);
+        sendCreate = true;
+      }
+
+      if (sendCreate) {
+        const newReportRequestId = v4();
+        // track the last 10 registration ids
+        reportMetadata.reportRequestIds = [
+          newReportRequestId,
+          ...reportMetadata.reportRequestIds,
+        ].slice(0, 10);
+        createRequests.push({
+          reportRequestId: newReportRequestId,
+          reportSpecifierId: reportMetadata.reportSpecifierId,
+          granularityDuration: `PT${reportSubscriptionParameters.dataGranularitySeconds}S`,
+          reportBackDuration: `PT${reportSubscriptionParameters.reportBackSeconds}S`,
+          startDate: new Date().toISOString(),
+          duration: `PT${reportSubscriptionParameters.subscriptionDurationSeconds}S`,
+          specifiers: reportMetadata.descriptions.map(description => ({
+            reportId: description.reportId,
+            readingType: 'x-notApplicable',
+          })),
+        });
+        reportMetadata.lastSentCreate = new Date().toISOString();
+      }
+    }
+    if (createRequests.length > 0) {
+      const createReport = {
+        _type: 'oadrCreateReport',
+        requestId: v4(),
+        requests: createRequests,
+      };
+      await nantum.updateReport(clientCertificateFingerprint, report);
+      return createReport;
+    }
+  }
+}
+
+async function registerReports(
+  oadrRegisterReport,
+  clientCertificateCn,
+  clientCertificateFingerprint,
+) {
+  logger.info(
+    'registerReports',
+    oadrRegisterReport,
+    clientCertificateCn,
+    clientCertificateFingerprint,
+  );
+
+  const requestVenId = oadrRegisterReport.venId;
+  validateVenId(requestVenId, clientCertificateFingerprint, false);
+
+  const venReportMetadata = (oadrRegisterReport.reports || []).map(report => {
+    const { reportSpecifierId, descriptions } = report;
+    const lastReceivedRegister = new Date().toISOString();
+    const reportRequestId = v4();
+    return {
+      reportRequestIds: [reportRequestId],
+      reportSpecifierId,
+      descriptions,
+      lastReceivedRegister,
+    };
+  });
+
+  //TODO: whitelist based off Nantum API sensors
+
+  await nantum.updateReport(clientCertificateFingerprint, {
+    venReportMetadata,
+  });
+
+  return {
+    _type: 'oadrRegisteredReport',
+    responseCode: '200',
+    responseRequestId: oadrRegisterReport.requestId,
+    responseDescription: 'OK',
+    requests: [],
+  };
+}
+
+async function createdReports(
+  oadrCreatedReport,
+  clientCertificateCn,
+  clientCertificateFingerprint,
+) {
+  logger.info(
+    'createdReports',
+    oadrCreatedReport,
+    clientCertificateCn,
+    clientCertificateFingerprint,
+  );
+
+  validateVenId(oadrCreatedReport.venId, clientCertificateFingerprint, false);
+
+  if (oadrCreatedReport.pendingReports) {
+    // flag reports as having been created
+    const report = await nantum.fetchReport(clientCertificateFingerprint);
+    if (report.venReportMetadata) {
+      for (const pendingReport of oadrCreatedReport.pendingReports) {
+        const reportRequestId = pendingReport['reportRequestId'];
+        const match = report.venReportMetadata.filter(x =>
+          x.reportRequestIds.includes(reportRequestId),
+        )[0];
+        if (match) {
+          match.lastReceivedCreated = new Date().toISOString();
+        } else {
+          logger.info(
+            'could not match',
+            reportRequestId,
+            report.venReportMetadata,
+          );
+        }
+      }
+    }
+    await nantum.updateReport(clientCertificateFingerprint, report);
+  }
+
+  return {
+    _type: 'oadrResponse',
+    responseCode: '200',
+    responseDescription: 'OK',
+    venId: clientCertificateFingerprint,
+  };
+}
+
+async function receiveReportData(
+  oadrUpdateReport,
+  clientCertificateCn,
+  clientCertificateFingerprint,
+) {
+  logger.info(
+    'receiveReportData',
+    oadrUpdateReport,
+    clientCertificateCn,
+    clientCertificateFingerprint,
+  );
+
+  const requestVenId = oadrUpdateReport.venId;
+  validateVenId(requestVenId, clientCertificateFingerprint, false);
+
+  const report = await nantum.fetchReport(clientCertificateFingerprint);
+  if (report.venReportMetadata) {
+    for (const updateReport of oadrUpdateReport.reports) {
+      const reportRequestId = updateReport.reportRequestId;
+      const match = report.venReportMetadata.filter(x =>
+        x.reportRequestIds.includes(reportRequestId),
+      )[0];
+      if (!match) {
+        logger.info(
+          'could not match',
+          reportRequestId,
+          report.venReportMetadata,
+        );
+        continue;
+      }
+      match.lastReceivedUpdate = new Date().toISOString();
+      for (const interval of updateReport.intervals || []) {
+        const reportId = interval.reportPayloads[0].reportId;
+        const date = interval.startDate;
+
+        if (interval.reportPayloads[0].payloadFloat) {
+          const value = interval.reportPayloads[0].payloadFloat;
+          logger.info('received report', [
+            date,
+            clientCertificateFingerprint,
+            updateReport.reportSpecifierId,
+            updateReport.reportName,
+            reportRequestId,
+            reportId,
+            value,
+          ]);
+        }
+        if (
+          interval.reportPayloads[0].payloadStatus &&
+          interval.reportPayloads[0].payloadStatus.loadControlState
+        ) {
+          const loadControlState =
+            interval.reportPayloads[0].payloadStatus.loadControlState;
+          Object.keys(loadControlState).forEach(type => {
+            const typeObj = loadControlState[type];
+            Object.keys(typeObj).forEach(subType => {
+              const value = typeObj[subType];
+              logger.info('received report', [
+                date,
+                clientCertificateFingerprint,
+                updateReport.reportSpecifierId,
+                updateReport.reportName,
+                reportRequestId,
+                reportId,
+                type,
+                subType,
+                value,
+              ]);
+            });
+          });
+        }
+      }
+    }
+  }
+  await nantum.updateReport(clientCertificateFingerprint, report);
+
+  return {
+    _type: 'oadrUpdatedReport',
+    responseCode: '200',
+    responseRequestId: oadrUpdateReport.requestId,
+    responseDescription: 'OK',
+    venId: clientCertificateFingerprint,
+  };
+}
+
+function validateVenId(requestVenId, clientCertificateFingerprint, required) {
+  if (requestVenId === clientCertificateFingerprint) {
+    return;
+  }
+  if (!required && requestVenId == null) {
+    return;
+  }
+  if (required && requestVenId == null) {
+    const error = new Error('VenID is missing');
+    error.responseCode = 452;
+    throw error;
+  }
+  const error = new Error('VenID does not match certificate');
+  error.responseCode = 452;
+  throw error;
+}
+
+module.exports = {
+  registerReports,
+  createdReports,
+  pollForReports,
+  receiveReportData,
+};

+ 12 - 12
server/controllers/poll.js

@@ -2,19 +2,13 @@
 
 const logger = require('../../logger');
 const { pollForEvents } = require('../../processes/event');
+const { pollForReports } = require('../../processes/report');
 
-const {
-  serialize: serializeOadrResponse,
-} = require('../../xml/poll/oadr-response');
-
-const {
-  serialize: serializeDistributeEvent,
-} = require('../../xml/event/distribute-event');
+const { serialize } = require('../../xml');
 
 exports.postController = async (req, res) => {
   const parsedRequest = req.xml;
   let xmlResponse;
-  let serialize = serializeOadrResponse;
 
   try {
     let jsonResponse;
@@ -25,16 +19,21 @@ exports.postController = async (req, res) => {
           req.clientCertificateCn,
           req.clientCertificateFingerprint,
         );
+        if (!jsonResponse) {
+          jsonResponse = await pollForReports(
+            parsedRequest,
+            req.clientCertificateCn,
+            req.clientCertificateFingerprint,
+          );
+        }
         break;
       default:
         throw new Error(`Unknown _type: ${parsedRequest._type}`);
     }
 
-    if (jsonResponse) {
-      serialize = serializeDistributeEvent;
-    } else {
-      serialize = serializeOadrResponse;
+    if (!jsonResponse) {
       jsonResponse = {
+        _type: 'oadrResponse',
         responseCode: '200',
         responseDescription: 'OK',
         responseRequestId: parsedRequest.requestId || '',
@@ -48,6 +47,7 @@ exports.postController = async (req, res) => {
     const responseRequestId =
       parsedRequest != null ? parsedRequest.requestId : '';
     xmlResponse = serialize({
+      _type: 'oadrResponse',
       responseCode: e.responseCode || '454',
       responseDescription: e.message || 'Unknown error',
       responseRequestId: responseRequestId || '',

+ 63 - 0
server/controllers/report.js

@@ -0,0 +1,63 @@
+'use strict';
+
+const logger = require('../../logger');
+const { serialize } = require('../../xml');
+
+const {
+  createdReports,
+  registerReports,
+  receiveReportData,
+} = require('../../processes/report');
+
+exports.postController = async (req, res) => {
+  const parsedRequest = req.xml;
+  let xmlResponse;
+
+  try {
+    let response;
+    switch (parsedRequest._type) {
+      case 'oadrRegisterReport':
+        response = await registerReports(
+          parsedRequest,
+          req.clientCertificateCn,
+          req.clientCertificateFingerprint,
+        );
+        break;
+      case 'oadrCreatedReport':
+        response = await createdReports(
+          parsedRequest,
+          req.clientCertificateCn,
+          req.clientCertificateFingerprint,
+        );
+        break;
+      case 'oadrUpdateReport':
+        response = await receiveReportData(
+          parsedRequest,
+          req.clientCertificateCn,
+          req.clientCertificateFingerprint,
+        );
+        break;
+      default:
+        throw new Error(`Unknown _type: ${parsedRequest._type}`);
+    }
+    xmlResponse = serialize(response);
+  } catch (e) {
+    logger.warn('Error occurred processing', parsedRequest || req.xml, e);
+    const responseRequestId =
+      parsedRequest != null ? parsedRequest.requestId : '';
+    xmlResponse = serialize({
+      _type: 'oadrResponse',
+      responseCode: e.responseCode || '454',
+      responseDescription: e.message || 'Unknown error',
+      responseRequestId: responseRequestId || '',
+    });
+  }
+  res.set('Content-Type', 'application/xml');
+  res.send(xmlResponse);
+  res.end();
+};
+
+exports.postErrorHandler = (error, next) => {
+  logger.warn('Error in EiReport', { error });
+  next(error);
+};

+ 2 - 0
server/middleware/xml-parser.js

@@ -1,11 +1,13 @@
 'use strict';
 
+const logger = require('../../logger');
 const { parse } = require('../../xml');
 
 module.exports = async (req, res, next) => {
   try {
     req.xml = await parse(req.body);
   } catch (e) {
+    logger.info('Error parsing body', req.body, e);
     return next(e);
   }
   return next();

+ 1 - 0
server/routes/index.js

@@ -6,6 +6,7 @@ const oadrPrefix = '/OpenADR2/Simple/2.0b';
 
 router.use(`${oadrPrefix}/EiRegisterParty`, require('./register-party'));
 router.use(`${oadrPrefix}/EiEvent`, require('./event'));
+router.use(`${oadrPrefix}/EiReport`, require('./report'));
 router.use(`${oadrPrefix}/OadrPoll`, require('./poll'));
 
 module.exports = router;

+ 10 - 0
server/routes/report.js

@@ -0,0 +1,10 @@
+'use strict';
+
+const router = require('express').Router();
+
+const asyncHandler = require('../middleware/async-handler');
+const { postController, postErrorHandler } = require('../controllers/report');
+
+router.post('/', asyncHandler(postController, postErrorHandler));
+
+module.exports = router;

+ 15 - 43
xml/event/created-event.js

@@ -1,21 +1,15 @@
 'use strict';
 
 const { parseXML, childAttr, required, number } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
-
-function parseEiResponse(response) {
-  return {
-    code: required(childAttr(response, 'responseCode'), 'responseCode'),
-    description: childAttr(response, 'responseDescription'),
-    requestId: required(childAttr(response, 'requestID'), 'requestID'),
-  };
-}
+const {
+  createDoc,
+  parseEiResponse,
+  serializeEiResponse,
+  energyInteropNs,
+  energyInteropPayloadsNs,
+  oadrPayloadNs,
+} = require('../shared');
+const { fragment } = require('xmlbuilder2');
 
 function parseEventResponse(eventResponse) {
   const result = {
@@ -87,26 +81,6 @@ async function parse(input) {
   return parseEiCreatedEvent(o['eiCreatedEvent'][0]['$$']);
 }
 
-function serializeEiResponse(eiCreatedEvent) {
-  const descriptionFrag =
-    eiCreatedEvent.responseDescription != null
-      ? fragment()
-        .ele(energyInteropNs, 'ei:responseDescription')
-        .txt(eiCreatedEvent.responseDescription)
-      : fragment();
-
-  return fragment()
-    .ele(energyInteropNs, 'ei:eiResponse')
-    .ele(energyInteropNs, 'ei:responseCode')
-    .txt(eiCreatedEvent.responseCode)
-    .up()
-    .import(descriptionFrag)
-    .ele(energyInteropPayloadsNs, 'pyld:requestID')
-    .txt(eiCreatedEvent.responseRequestId)
-    .up()
-    .up();
-}
-
 function serializeEventResponse(eventResponse) {
   const descriptionFrag =
     eventResponse.responseDescription != null
@@ -165,14 +139,7 @@ function serializeEiCreatedEvent(eiCreatedEvent) {
 }
 
 function serialize(obj) {
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrCreatedEvent')
@@ -187,8 +154,13 @@ async function canParse(input) {
   return o['oadrCreatedEvent'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrCreatedEvent';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 39 - 176
xml/event/distribute-event.js

@@ -9,19 +9,27 @@ const {
   duration,
   number,
 } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const xsiNs = 'http://www.w3.org/2001/XMLSchema-instance';
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const emixNs = 'http://docs.oasis-open.org/ns/emix/2011/06';
-const powerNs = 'http://docs.oasis-open.org/ns/emix/2011/06/power';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
-const calendarNs = 'urn:ietf:params:xml:ns:icalendar-2.0';
-const calendarStreamNs = 'urn:ietf:params:xml:ns:icalendar-2.0:stream';
-const siScaleNs = 'http://docs.oasis-open.org/ns/emix/2011/06/siscale';
+const {
+  createDoc,
+  parseEiResponse,
+  parseEventSignalIntervals,
+  serializeEventSignalIntervals,
+  parsePayloadFloat,
+  serializePayloadFloat,
+  serializeEiResponse,
+  serializeDuration,
+  serializeDateTime,
+  energyInteropPayloadsNs,
+  energyInteropNs,
+  oadrNs,
+  powerNs,
+  emixNs,
+  calendarNs,
+  calendarStreamNs,
+  siScaleNs,
+  xsiNs,
+} = require('../shared');
+const { fragment } = require('xmlbuilder2');
 
 const eiTargetMappings = [
   {
@@ -283,14 +291,6 @@ function serializeItemBase(x) {
   }
 }
 
-function parseEiResponse(response) {
-  return {
-    code: required(childAttr(response, 'responseCode'), 'responseCode'),
-    description: childAttr(response, 'responseDescription'),
-    requestId: required(childAttr(response, 'requestID'), 'requestID'),
-  };
-}
-
 function parseEventDescriptor(eventDescriptor) {
   const result = {
     eventId: required(childAttr(eventDescriptor, 'eventID'), 'eventID'),
@@ -405,108 +405,6 @@ function parseToleranceTolerateStartAfter(tolerance) {
   }
 }
 
-function parsePayloadFloat(payloadFloatInput) {
-  const payloadFloat = required(
-    childAttr(payloadFloatInput, 'payloadFloat'),
-    'payloadFloat',
-  )['$$'];
-  return required(number(childAttr(payloadFloat, 'value')), 'value');
-}
-
-function serializePayloadFloat(payloadFloat) {
-  const result = fragment();
-  result
-    .ele(energyInteropNs, 'ei:payloadFloat')
-    .ele(energyInteropNs, 'ei:value')
-    .txt(payloadFloat);
-  return result;
-}
-
-function serializeSignalPayload(signalPayload) {
-  const result = fragment();
-  result
-    .ele(energyInteropNs, 'ei:signalPayload')
-    .import(serializePayloadFloat(signalPayload));
-  return result;
-}
-
-function parseSignalPayloads(signalPayloads) {
-  return signalPayloads.map(x => parsePayloadFloat(x['$$']));
-}
-
-function serializeSignalPayloads(signalPayloads) {
-  return signalPayloads.map(x => serializeSignalPayload(x));
-}
-
-function parseEventSignalInterval(eventSignalInterval) {
-  const result = {
-    signalPayloads: parseSignalPayloads(eventSignalInterval.signalPayload),
-  };
-
-  const durationValue = duration(
-    childAttr(eventSignalInterval, 'duration'),
-    'duration',
-  );
-  if (durationValue != null) result.duration = durationValue;
-
-  const dtStartValue = dateTime(
-    childAttr(eventSignalInterval, 'dtstart'),
-    'date-time',
-  );
-  if (dtStartValue != null) {
-    result.startDate = dtStartValue;
-  }
-
-  const uidHolder = childAttr(eventSignalInterval, 'uid');
-  if (uidHolder != null) {
-    result.uid = required(childAttr(uidHolder['$$'], 'text'));
-  }
-
-  return result;
-}
-
-function serializeEventSignalInterval(eventSignalInterval) {
-  const result = fragment();
-  const interval = result.ele(energyInteropNs, 'ei:interval');
-
-  if (eventSignalInterval.duration) {
-    interval
-      .ele(calendarNs, 'cal:duration')
-      .import(serializeDuration(eventSignalInterval.duration));
-  }
-
-  if (eventSignalInterval.startDate) {
-    interval
-      .ele(calendarNs, 'cal:dtstart')
-      .import(serializeDateTime(eventSignalInterval.startDate));
-  }
-
-  if (eventSignalInterval.uid) {
-    interval
-      .ele(calendarNs, 'cal:uid')
-      .ele(calendarNs, 'cal:text')
-      .txt(eventSignalInterval.uid);
-  }
-
-  serializeSignalPayloads(eventSignalInterval.signalPayloads).forEach(payload =>
-    interval.import(payload),
-  );
-  return result;
-}
-
-function parseEventSignalIntervals(eventSignalIntervals) {
-  if (!eventSignalIntervals) {
-    return [];
-  }
-  return eventSignalIntervals['interval'].map(x =>
-    parseEventSignalInterval(x['$$']),
-  );
-}
-
-function serializeEventSignalIntervals(eventSignalIntervals) {
-  return eventSignalIntervals.map(x => serializeEventSignalInterval(x));
-}
-
 function parseEiTarget(eiTarget) {
   const result = {};
   for (const eiTargetMapping of eiTargetMappings) {
@@ -557,12 +455,16 @@ function serializeEiTarget(eiTarget) {
 
 function parseEventSignal(eventSignal) {
   const result = {
-    intervals: parseEventSignalIntervals(eventSignal['intervals'][0]['$$']),
     signalName: required(childAttr(eventSignal, 'signalName'), 'signalName'),
     signalType: required(childAttr(eventSignal, 'signalType'), 'signalType'),
     signalId: required(childAttr(eventSignal, 'signalID'), 'signalID'),
   };
 
+  const intervals = eventSignal['intervals'][0]['$$'];
+  if (intervals) {
+    result.intervals = parseEventSignalIntervals(intervals['interval']);
+  }
+
   const eiTarget = childAttr(eventSignal, 'eiTarget');
   if (eiTarget != null) {
     result.target = parseEiTarget(eiTarget['$$']);
@@ -623,7 +525,6 @@ function parseEventBaseline(eventSignal) {
       duration(childAttr(eventSignal, 'duration'), 'duration'),
       'duration',
     ),
-    intervals: parseEventSignalIntervals(eventSignal['intervals'][0]['$$']),
     baselineId: required(childAttr(eventSignal, 'baselineID'), 'baselineID'),
     baselineName: required(
       childAttr(eventSignal, 'baselineName'),
@@ -631,6 +532,11 @@ function parseEventBaseline(eventSignal) {
     ),
   };
 
+  const intervals = eventSignal['intervals'][0]['$$'];
+  if (intervals) {
+    result.intervals = parseEventSignalIntervals(intervals['interval']);
+  }
+
   const itemBase = parseItemBase(eventSignal);
   if (itemBase != null) result.itemBase = itemBase;
 
@@ -744,13 +650,6 @@ function parseEiActivePeriod(activePeriod) {
   return result;
 }
 
-function serializeDateTime(dateTime) {
-  return fragment()
-    .ele(calendarNs, 'cal:date-time')
-    .txt(dateTime)
-    .up();
-}
-
 function serializeToleranceTolerateStartAfter(duration) {
   const result = fragment();
   result
@@ -885,31 +784,6 @@ async function parse(input) {
   return result;
 }
 
-function serializeEiResponse(code, description, requestId) {
-  const descriptionFrag =
-    description != null
-      ? fragment()
-        .ele(energyInteropNs, 'ei:responseDescription')
-        .txt(description)
-      : fragment();
-  return fragment()
-    .ele(energyInteropNs, 'ei:responseCode')
-    .txt(code)
-    .up()
-    .import(descriptionFrag)
-    .ele(energyInteropPayloadsNs, 'pyld:requestID')
-    .txt(requestId)
-    .up();
-}
-
-function serializeDuration(duration) {
-  return duration != null
-    ? fragment()
-      .ele(calendarNs, 'cal:duration')
-      .txt(duration)
-    : fragment();
-}
-
 function validate(obj) {
   if (obj.responseCode == null) {
     throw new Error('Missing responseCode');
@@ -951,32 +825,16 @@ function serialize(obj) {
   const requestId =
     obj.requestId != null
       ? fragment()
-        .ele(oadrPayloadNs, 'pyld:requestID')
+        .ele(energyInteropPayloadsNs, 'pyld:requestID')
         .txt(obj.requestId)
       : fragment();
 
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-      cal: calendarNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrDistributeEvent')
     .att('@ei', 'ei:schemaVersion', '2.0b')
-    .ele('@ei', 'ei:eiResponse')
-    .import(
-      serializeEiResponse(
-        obj.responseCode,
-        obj.responseDescription,
-        obj.responseRequestId,
-      ),
-    )
-    .up()
+    .import(serializeEiResponse(obj))
     .import(vtnId)
     .import(requestId)
     .import(serializeEvents(obj.events))
@@ -990,8 +848,13 @@ async function canParse(input) {
   return o['oadrDistributeEvent'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrDistributeEvent';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 12 - 15
xml/event/request-event.js

@@ -1,13 +1,12 @@
 'use strict';
 
 const { parseXML, childAttr, required } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
+const { fragment } = require('xmlbuilder2');
+const {
+  createDoc,
+  energyInteropPayloadsNs,
+  energyInteropNs,
+} = require('../shared');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -47,14 +46,7 @@ function serializeEiRequestEvent(requestId, venId, replyLimit) {
 }
 
 function serialize(obj) {
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrRequestEvent')
@@ -71,8 +63,13 @@ async function canParse(input) {
   return o['oadrRequestEvent'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrRequestEvent';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 11 - 0
xml/index.js

@@ -2,6 +2,7 @@
 
 const parsers = [
   ...require('./event'),
+  ...require('./report'),
   ...require('./poll'),
   ...require('./register-party'),
 ];
@@ -15,6 +16,16 @@ async function parse(input) {
   throw new Error(`No parser for input: ${input}`);
 }
 
+function serialize(message) {
+  for (const parser of parsers) {
+    if (parser.canSerialize(message)) {
+      return parser.serialize(message);
+    }
+  }
+  throw new Error(`No serializer for message: ${message}`);
+}
+
 module.exports = {
   parse,
+  serialize,
 };

+ 1 - 1
xml/parser.js

@@ -16,7 +16,7 @@ function childAttr(obj, key, errorCode = 452) {
     return undefined;
   }
 
-  if (value.length !== 1) {
+  if (value.length < 0) {
     const error = new Error(`Invalid attribute: ${key}`);
     error.errorCode = errorCode;
     throw error;

+ 7 - 12
xml/poll/oadr-poll.js

@@ -1,11 +1,7 @@
 'use strict';
 
 const { parseXML, childAttr, required } = require('../parser');
-const { create } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
+const { createDoc, energyInteropNs } = require('../shared');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -19,13 +15,7 @@ async function parse(input) {
 }
 
 function serialize(obj) {
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrPoll')
@@ -43,8 +33,13 @@ async function canParse(input) {
   return o['oadrPoll'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrPoll';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 15 - 56
xml/poll/oadr-response.js

@@ -1,27 +1,13 @@
 'use strict';
 
-const { parseXML, childAttr, required } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
-
-function parseEiResponse(response) {
-  const result = {
-    code: required(childAttr(response, 'responseCode'), 'responseCode'),
-    description: childAttr(response, 'responseDescription'),
-  };
-
-  const requestId = childAttr(response, 'requestID');
-  if (requestId != null) {
-    result.requestId = requestId;
-  }
-
-  return result;
-}
+const { parseXML, childAttr } = require('../parser');
+const {
+  createDoc,
+  energyInteropNs,
+  parseEiResponse,
+  serializeEiResponse,
+} = require('../shared');
+const { fragment } = require('xmlbuilder2');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -52,23 +38,6 @@ async function parse(input) {
   return result;
 }
 
-function serializeEiResponse(code, description, requestId) {
-  const descriptionFrag =
-    description != null
-      ? fragment()
-        .ele(energyInteropNs, 'ei:responseDescription')
-        .txt(description)
-      : fragment();
-  return fragment()
-    .ele(energyInteropNs, 'ei:responseCode')
-    .txt(code)
-    .up()
-    .import(descriptionFrag)
-    .ele(energyInteropPayloadsNs, 'pyld:requestID')
-    .txt(requestId || '')
-    .up();
-}
-
 function validate(obj) {
   if (!obj.responseCode) {
     throw new Error('Missing responseCode');
@@ -85,27 +54,12 @@ function serialize(obj) {
         .txt(obj.venId)
       : fragment();
 
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrResponse')
     .att('@ei', 'ei:schemaVersion', '2.0b')
-    .ele('@ei', 'ei:eiResponse')
-    .import(
-      serializeEiResponse(
-        obj.responseCode,
-        obj.responseDescription,
-        obj.responseRequestId,
-      ),
-    )
-    .up()
+    .import(serializeEiResponse(obj))
     .import(venId)
     .doc();
   return doc.end({ headless: true, prettyPrint: false });
@@ -117,8 +71,13 @@ async function canParse(input) {
   return o['oadrResponse'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrResponse';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 8 - 17
xml/register-party/cancel-party-registration.js

@@ -1,14 +1,8 @@
 'use strict';
 
 const { parseXML, childAttr, required } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
-const calendarNs = 'urn:ietf:params:xml:ns:icalendar-2.0';
+const { createDoc, energyInteropNs } = require('../shared');
+const { fragment } = require('xmlbuilder2');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -37,15 +31,7 @@ function serialize(obj) {
         .txt(obj.venId)
       : fragment();
 
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-      cal: calendarNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrCancelPartyRegistration')
@@ -67,8 +53,13 @@ async function canParse(input) {
   return o['oadrCancelPartyRegistration'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrCancelPartyRegistration';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 15 - 50
xml/register-party/canceled-party-registration.js

@@ -1,21 +1,13 @@
 'use strict';
 
-const { parseXML, childAttr, required } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
-
-function parseEiResponse(response) {
-  return {
-    code: required(childAttr(response, 'responseCode'), 'responseCode'),
-    description: childAttr(response, 'responseDescription'),
-    requestId: required(childAttr(response, 'requestID'), 'requestID'),
-  };
-}
+const { parseXML, childAttr } = require('../parser');
+const { fragment } = require('xmlbuilder2');
+const {
+  createDoc,
+  energyInteropNs,
+  parseEiResponse,
+  serializeEiResponse,
+} = require('../shared');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -51,23 +43,6 @@ async function parse(input) {
   return result;
 }
 
-function serializeEiResponse(code, description, requestId) {
-  const descriptionFrag =
-    description != null
-      ? fragment()
-        .ele(energyInteropNs, 'ei:responseDescription')
-        .txt(description)
-      : fragment();
-  return fragment()
-    .ele(energyInteropNs, 'ei:responseCode')
-    .txt(code)
-    .up()
-    .import(descriptionFrag)
-    .ele(energyInteropPayloadsNs, 'pyld:requestID')
-    .txt(requestId)
-    .up();
-}
-
 function validate(obj) {
   if (!obj.responseCode) {
     throw new Error('Missing responseCode');
@@ -93,27 +68,12 @@ function serialize(obj) {
         .txt(obj.venId)
       : fragment();
 
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrCanceledPartyRegistration')
     .att('@ei', 'ei:schemaVersion', '2.0b')
-    .ele('@ei', 'ei:eiResponse')
-    .import(
-      serializeEiResponse(
-        obj.responseCode,
-        obj.responseDescription,
-        obj.responseRequestId,
-      ),
-    )
-    .up()
+    .import(serializeEiResponse(obj))
     .import(registrationId)
     .import(venId)
     .doc();
@@ -126,8 +86,13 @@ async function canParse(input) {
   return o['oadrCanceledPartyRegistration'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrCanceledPartyRegistration';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 8 - 17
xml/register-party/create-party-registration.js

@@ -1,14 +1,8 @@
 'use strict';
 
 const { parseXML, childAttr, boolean, required } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
-const calendarNs = 'urn:ietf:params:xml:ns:icalendar-2.0';
+const { fragment } = require('xmlbuilder2');
+const { createDoc, energyInteropNs, oadrNs } = require('../shared');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -86,15 +80,7 @@ function serialize(obj) {
         .txt(obj.oadrHttpPullModel)
       : fragment();
 
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-      cal: calendarNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrCreatePartyRegistration')
@@ -129,8 +115,13 @@ async function canParse(input) {
   return o['oadrCreatePartyRegistration'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrCreatePartyRegistration';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 18 - 65
xml/register-party/created-party-registration.js

@@ -1,26 +1,14 @@
 'use strict';
 
 const { parseXML, childAttr, required } = require('../parser');
-const { create, fragment } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
-const calendarNs = 'urn:ietf:params:xml:ns:icalendar-2.0';
-
-function parseEiResponse(response) {
-  return {
-    code: required(childAttr(response, 'responseCode'), 'responseCode'),
-    description: childAttr(response, 'responseDescription'),
-    requestId: required(childAttr(response, 'requestID'), 'requestID'),
-  };
-}
-
-function parseDuration(response) {
-  return required(childAttr(response, 'duration'), 'duration');
-}
+const { fragment } = require('xmlbuilder2');
+const {
+  createDoc,
+  energyInteropNs,
+  serializeEiResponse,
+  serializeDuration,
+  parseEiResponse,
+} = require('../shared');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -58,8 +46,9 @@ async function parse(input) {
 
   const oadrRequestedOadrPollFreq = childAttr(o, 'oadrRequestedOadrPollFreq');
   if (oadrRequestedOadrPollFreq != null) {
-    const oadrRequestedOadrPollFreqDuration = parseDuration(
-      oadrRequestedOadrPollFreq['$$'],
+    const oadrRequestedOadrPollFreqDuration = required(
+      childAttr(oadrRequestedOadrPollFreq['$$'], 'duration'),
+      'duration',
     );
     if (oadrRequestedOadrPollFreqDuration != null)
       result.pollFreqDuration = oadrRequestedOadrPollFreqDuration;
@@ -68,31 +57,6 @@ async function parse(input) {
   return result;
 }
 
-function serializeEiResponse(code, description, requestId) {
-  const descriptionFrag =
-    description != null
-      ? fragment()
-        .ele(energyInteropNs, 'ei:responseDescription')
-        .txt(description)
-      : fragment();
-  return fragment()
-    .ele(energyInteropNs, 'ei:responseCode')
-    .txt(code)
-    .up()
-    .import(descriptionFrag)
-    .ele(energyInteropPayloadsNs, 'pyld:requestID')
-    .txt(requestId)
-    .up();
-}
-
-function serializeDuration(duration) {
-  return duration != null
-    ? fragment()
-      .ele(calendarNs, 'cal:duration')
-      .txt(duration)
-    : fragment();
-}
-
 function validate(obj) {
   if (!obj.responseCode) {
     throw new Error('Missing responseCode');
@@ -124,28 +88,12 @@ function serialize(obj) {
         .txt(obj.vtnId)
       : fragment();
 
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-      cal: calendarNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrCreatedPartyRegistration')
     .att('@ei', 'ei:schemaVersion', '2.0b')
-    .ele('@ei', 'ei:eiResponse')
-    .import(
-      serializeEiResponse(
-        obj.responseCode,
-        obj.responseDescription,
-        obj.responseRequestId,
-      ),
-    )
-    .up()
+    .import(serializeEiResponse(obj))
     .import(registrationId)
     .import(venId)
     .import(vtnId)
@@ -175,8 +123,13 @@ async function canParse(input) {
   return o['oadrCreatedPartyRegistration'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrCreatedPartyRegistration';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 7 - 15
xml/register-party/query-registration.js

@@ -1,13 +1,7 @@
 'use strict';
 
 const { parseXML, childAttr, required } = require('../parser');
-const { create } = require('xmlbuilder2');
-
-const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
-const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
-const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
-const energyInteropPayloadsNs =
-  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
+const { createDoc, energyInteropNs } = require('../shared');
 
 async function parse(input) {
   const json = await parseXML(input);
@@ -23,14 +17,7 @@ async function parse(input) {
 }
 
 function serialize(obj) {
-  const doc = create({
-    namespaceAlias: {
-      ns: oadrPayloadNs,
-      oadr2b: oadrNs,
-      ei: energyInteropNs,
-      pyld: energyInteropPayloadsNs,
-    },
-  })
+  const doc = createDoc()
     .ele('@oadr2b', 'oadr2b:oadrPayload')
     .ele('oadr2b:oadrSignedObject')
     .ele('oadr2b:oadrQueryRegistration')
@@ -47,8 +34,13 @@ async function canParse(input) {
   return o['oadrQueryRegistration'] != null;
 }
 
+function canSerialize(message) {
+  return message._type === 'oadrQueryRegistration';
+}
+
 module.exports = {
   parse,
   serialize,
   canParse,
+  canSerialize,
 };

+ 66 - 0
xml/report/create-report.js

@@ -0,0 +1,66 @@
+'use strict';
+
+const { parseXML, childAttr, required } = require('../parser');
+const {
+  createDoc,
+  serializeReportRequests,
+  parseReportRequests,
+} = require('../shared');
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrCreateReport'
+    ][0]['$$'];
+
+  const requestId = required(childAttr(o, 'requestID'), 'requestID');
+
+  const result = {
+    _type: 'oadrCreateReport',
+    requestId: requestId,
+  };
+
+  result.requests = parseReportRequests(o['oadrReportRequest'] || []);
+  return result;
+}
+
+function validate(obj) {
+  if (!obj.requestId) {
+    throw new Error('Missing requestId');
+  }
+}
+
+function serialize(obj) {
+  validate(obj);
+
+  const doc = createDoc()
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrCreateReport')
+    .att('@ei', 'ei:schemaVersion', '2.0b')
+    .ele('@pyld', 'pyld:requestID')
+    .txt(obj.requestId)
+    .up()
+    .import(serializeReportRequests(obj.requests))
+    .up()
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+async function canParse(input) {
+  const json = await parseXML(input);
+  const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'];
+  return o['oadrCreateReport'] != null;
+}
+
+function canSerialize(message) {
+  return message._type === 'oadrCreateReport';
+}
+
+module.exports = {
+  parse,
+  serialize,
+  canParse,
+  canSerialize,
+};

+ 112 - 0
xml/report/created-report.js

@@ -0,0 +1,112 @@
+'use strict';
+
+const { parseXML, childAttr, required } = require('../parser');
+const {
+  createDoc,
+  parseEiResponse,
+  serializeEiResponse,
+  oadrNs,
+  energyInteropNs,
+} = require('../shared');
+const { fragment } = require('xmlbuilder2');
+
+function parsePendingReport(pendingReport) {
+  return {
+    reportRequestId: required(
+      childAttr(pendingReport, 'reportRequestID'),
+      'reportRequestID',
+    ),
+  };
+}
+
+function parsePendingReports(pendingReports) {
+  return pendingReports.map(x => parsePendingReport(x['$$']));
+}
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrCreatedReport'
+    ][0]['$$'];
+
+  const { code, description, requestId } = parseEiResponse(
+    o['eiResponse'][0]['$$'],
+  );
+
+  const result = {
+    _type: 'oadrCreatedReport',
+    responseCode: code,
+    responseRequestId: requestId
+  };
+
+  if (description != null) result.responseDescription = description;
+
+  if (result.responseCode < 200 || result.responseCode >= 300) {
+    throw new Error(result.responseDescription || result.responseCode);
+  }
+
+  if (o['oadrPendingReports']) {
+    result.pendingReports = parsePendingReports(o['oadrPendingReports']);
+  }
+
+  const venId = childAttr(o, 'venID');
+  if (venId != null) {
+    result.venId = venId;
+  }
+
+  return result;
+}
+
+function serializePendingReport(pendingReport) {
+  return fragment()
+    .ele(energyInteropNs, 'ei:reportRequestID')
+    .txt(pendingReport.reportRequestId)
+    .up();
+}
+
+function serializePendingReports(pendingReports) {
+  const result = fragment();
+  const responsesHolder = result.ele(oadrNs, 'oadr2b:oadrPendingReports');
+
+  pendingReports.forEach(x =>
+    responsesHolder.import(serializePendingReport(x)),
+  );
+  return result;
+}
+
+function serialize(obj) {
+  const venId =
+    obj.venId != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:venID')
+        .txt(obj.venId)
+      : fragment();
+
+  const doc = createDoc()
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrCreatedReport')
+    .import(serializeEiResponse(obj))
+    .import(serializePendingReports(obj.pendingReports))
+    .import(venId)
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+async function canParse(input) {
+  const json = await parseXML(input);
+  const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'];
+  return o['oadrCreatedReport'] != null;
+}
+
+function canSerialize(message) {
+  return message._type === 'oadrCreatedReport';
+}
+
+module.exports = {
+  parse,
+  serialize,
+  canParse,
+  canSerialize,
+};

+ 10 - 0
xml/report/index.js

@@ -0,0 +1,10 @@
+'use strict';
+
+module.exports = [
+  require('./create-report'),
+  require('./created-report'),
+  require('./register-report'),
+  require('./registered-report'),
+  require('./update-report'),
+  require('./updated-report'),
+];

+ 68 - 0
xml/report/register-report.js

@@ -0,0 +1,68 @@
+'use strict';
+
+const { parseXML, childAttr, required } = require('../parser');
+const {
+  createDoc,
+  parseReports,
+  serializeReports,
+  energyInteropNs,
+} = require('../shared');
+const { fragment } = require('xmlbuilder2');
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrRegisterReport'
+    ][0]['$$'];
+
+  const result = {
+    _type: 'oadrRegisterReport',
+    requestId: required(childAttr(o, 'requestID'), 'requestID'),
+    reports: parseReports(o['oadrReport'] || []),
+  };
+
+  const venId = childAttr(o, 'venID');
+  if (venId != null) result.venId = venId;
+
+  return result;
+}
+
+function serialize(obj) {
+  const venId =
+    obj.venId != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:venID')
+        .txt(obj.venId)
+      : fragment();
+
+  const doc = createDoc()
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrRegisterReport')
+    .att(energyInteropNs, 'ei:schemaVersion', '2.0b')
+    .ele('@pyld', 'pyld:requestID')
+    .txt(obj.requestId)
+    .up()
+    .import(venId)
+    .import(serializeReports(obj.reports))
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+async function canParse(input) {
+  const json = await parseXML(input);
+  const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'];
+  return o['oadrRegisterReport'] != null;
+}
+
+function canSerialize(message) {
+  return message._type === 'oadrRegisterReport';
+}
+
+module.exports = {
+  parse,
+  serialize,
+  canParse,
+  canSerialize,
+};

+ 95 - 0
xml/report/registered-report.js

@@ -0,0 +1,95 @@
+'use strict';
+
+const { parseXML, childAttr } = require('../parser');
+const {
+  createDoc,
+  parseEiResponse,
+  serializeEiResponse,
+  energyInteropNs,
+  parseReportRequests,
+  serializeReportRequests,
+} = require('../shared');
+const { fragment } = require('xmlbuilder2');
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrRegisteredReport'
+    ][0]['$$'];
+
+  const { code, description, requestId } = parseEiResponse(
+    o['eiResponse'][0]['$$'],
+  );
+
+  const result = {
+    _type: 'oadrRegisteredReport',
+    responseCode: code,
+    responseRequestId: requestId,
+  };
+
+  if (description != null) {
+    result.responseDescription = description;
+  }
+
+  if (code < 200 || code >= 300) {
+    return result;
+  }
+
+  if (o['oadrReportRequest'] != null) {
+    result.requests = parseReportRequests(o['oadrReportRequest']);
+  }
+
+  const venId = childAttr(o, 'venID');
+  if (venId != null) result.venId = venId;
+
+  return result;
+}
+
+function validate(obj) {
+  if (!obj.responseCode) {
+    throw new Error('Missing responseCode');
+  }
+  if (!obj.responseRequestId) {
+    throw new Error('Missing responseRequestId');
+  }
+}
+
+function serialize(obj) {
+  validate(obj);
+
+  const venId =
+    obj.venId != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:venID')
+        .txt(obj.venId)
+      : fragment();
+
+  const doc = createDoc()
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrRegisteredReport')
+    .att('@ei', 'ei:schemaVersion', '2.0b')
+    .import(serializeEiResponse(obj))
+    .import(serializeReportRequests(obj.requests))
+    .import(venId)
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+async function canParse(input) {
+  const json = await parseXML(input);
+  const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'];
+  return o['oadrRegisteredReport'] != null;
+}
+
+function canSerialize(message) {
+  return message._type === 'oadrRegisteredReport';
+}
+
+module.exports = {
+  parse,
+  serialize,
+  canParse,
+  canSerialize,
+};

+ 62 - 0
xml/report/update-report.js

@@ -0,0 +1,62 @@
+'use strict';
+
+const { parseXML, childAttr, required } = require('../parser');
+const { createDoc, serializeReports, parseReports } = require('../shared');
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrUpdateReport'
+    ][0]['$$'];
+
+  const requestId = required(childAttr(o, 'requestID'), 'requestID');
+
+  const result = {
+    _type: 'oadrUpdateReport',
+    requestId: requestId,
+  };
+
+  result.reports = parseReports(o['oadrReport'] || []);
+  return result;
+}
+
+function validate(obj) {
+  if (!obj.requestId) {
+    throw new Error('Missing requestId');
+  }
+}
+
+function serialize(obj) {
+  validate(obj);
+
+  const doc = createDoc()
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrUpdateReport')
+    .att('@ei', 'ei:schemaVersion', '2.0b')
+    .ele('@pyld', 'pyld:requestID')
+    .txt(obj.requestId)
+    .up()
+    .import(serializeReports(obj.reports))
+    .up()
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+async function canParse(input) {
+  const json = await parseXML(input);
+  const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'];
+  return o['oadrUpdateReport'] != null;
+}
+
+function canSerialize(message) {
+  return message._type === 'oadrUpdateReport';
+}
+
+module.exports = {
+  parse,
+  serialize,
+  canParse,
+  canSerialize,
+};

+ 71 - 0
xml/report/updated-report.js

@@ -0,0 +1,71 @@
+'use strict';
+
+const { parseXML, childAttr } = require('../parser');
+const {
+  createDoc,
+  parseEiResponse,
+  serializeEiResponse,
+  energyInteropNs,
+} = require('../shared');
+const { fragment } = require('xmlbuilder2');
+
+async function parse(input) {
+  const json = await parseXML(input);
+  const o =
+    json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'][
+      'oadrUpdatedReport'
+    ][0]['$$'];
+
+  const { code, description, requestId } = parseEiResponse(
+    o['eiResponse'][0]['$$'],
+  );
+
+  const result = {
+    _type: 'oadrUpdatedReport',
+    responseCode: code,
+    responseRequestId: requestId,
+  };
+
+  if (description != null) result.responseDescription = description;
+  const venId = childAttr(o, 'venID');
+  if (venId != null) {
+    result.venId = venId;
+  }
+
+  return result;
+}
+
+function serialize(obj) {
+  const venId =
+    obj.venId != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:venID')
+        .txt(obj.venId)
+      : fragment();
+
+  const doc = createDoc()
+    .ele('@oadr2b', 'oadr2b:oadrPayload')
+    .ele('oadr2b:oadrSignedObject')
+    .ele('oadr2b:oadrUpdatedReport')
+    .import(serializeEiResponse(obj))
+    .import(venId)
+    .doc();
+  return doc.end({ headless: true, prettyPrint: false });
+}
+
+async function canParse(input) {
+  const json = await parseXML(input);
+  const o = json['oadrPayload']['$$']['oadrSignedObject'][0]['$$'];
+  return o['oadrUpdatedReport'] != null;
+}
+
+function canSerialize(message) {
+  return message._type === 'oadrUpdatedReport';
+}
+
+module.exports = {
+  parse,
+  serialize,
+  canParse,
+  canSerialize,
+};

+ 645 - 0
xml/shared.js

@@ -0,0 +1,645 @@
+'use strict';
+
+const {
+  childAttr,
+  required,
+  dateTime,
+  duration,
+  number,
+  boolean,
+} = require('./parser');
+const { create, fragment } = require('xmlbuilder2');
+
+const oadrPayloadNs = 'http://www.w3.org/2000/09/xmldsig#';
+const oadrNs = 'http://openadr.org/oadr-2.0b/2012/07';
+const energyInteropNs = 'http://docs.oasis-open.org/ns/energyinterop/201110';
+const energyInteropPayloadsNs =
+  'http://docs.oasis-open.org/ns/energyinterop/201110/payloads';
+const calendarNs = 'urn:ietf:params:xml:ns:icalendar-2.0';
+const calendarStreamNs = 'urn:ietf:params:xml:ns:icalendar-2.0:stream';
+const xsiNs = 'http://www.w3.org/2001/XMLSchema-instance';
+const emixNs = 'http://docs.oasis-open.org/ns/emix/2011/06';
+const powerNs = 'http://docs.oasis-open.org/ns/emix/2011/06/power';
+const siScaleNs = 'http://docs.oasis-open.org/ns/emix/2011/06/siscale';
+
+function serializeDateTime(dateTime) {
+  return fragment()
+    .ele(calendarNs, 'cal:date-time')
+    .txt(dateTime)
+    .up();
+}
+
+function serializeDuration(duration) {
+  return duration != null
+    ? fragment()
+      .ele(calendarNs, 'cal:duration')
+      .txt(duration)
+    : fragment();
+}
+
+function serializeEventSignalInterval(eventSignalInterval) {
+  const result = fragment();
+  const interval = result.ele(energyInteropNs, 'ei:interval');
+
+  if (eventSignalInterval.duration) {
+    interval
+      .ele(calendarNs, 'cal:duration')
+      .import(serializeDuration(eventSignalInterval.duration));
+  }
+
+  if (eventSignalInterval.startDate) {
+    interval
+      .ele(calendarNs, 'cal:dtstart')
+      .import(serializeDateTime(eventSignalInterval.startDate));
+  }
+
+  if (eventSignalInterval.uid) {
+    interval
+      .ele(calendarNs, 'cal:uid')
+      .ele(calendarNs, 'cal:text')
+      .txt(eventSignalInterval.uid);
+  }
+
+  if (eventSignalInterval.signalPayloads != null) {
+    serializeSignalPayloads(eventSignalInterval.signalPayloads).forEach(
+      payload => {
+        interval.import(payload);
+      },
+    );
+  }
+
+  if (eventSignalInterval.reportPayloads != null) {
+    serializeReportPayloads(eventSignalInterval.reportPayloads).forEach(
+      payload => {
+        interval.import(payload);
+      },
+    );
+  }
+  return result;
+}
+
+function serializeEventSignalIntervals(eventSignalIntervals) {
+  if (!eventSignalIntervals) return [];
+  return eventSignalIntervals.map(x => serializeEventSignalInterval(x));
+}
+
+function serializeSignalPayloads(signalPayloads) {
+  return signalPayloads.map(x => serializeSignalPayload(x));
+}
+
+function serializeReportPayloads(reportPayloads) {
+  return reportPayloads.map(x => serializeReportPayload(x));
+}
+
+function parsePayloadFloat(payloadFloatInput) {
+  const payloadFloat = required(
+    childAttr(payloadFloatInput, 'payloadFloat'),
+    'payloadFloat',
+  )['$$'];
+  return required(number(childAttr(payloadFloat, 'value')), 'value');
+}
+
+function serializePayloadFloat(payloadFloat) {
+  const result = fragment();
+  result
+    .ele(energyInteropNs, 'ei:payloadFloat')
+    .ele(energyInteropNs, 'ei:value')
+    .txt(payloadFloat);
+  return result;
+}
+
+function serializeLoadControlStateType(loadControlStateType) {
+  return Object.keys(loadControlStateType).reduce((accum, cur) => {
+    accum.ele('oadr2b', `oadr2b:${cur}`).txt(loadControlStateType[cur]);
+    return accum;
+  }, fragment());
+}
+
+function serializeLoadControlState(loadControlState) {
+  const result = fragment();
+  const resultChildren = Object.keys(loadControlState).reduce((accum, cur) => {
+    accum
+      .ele('oadr2b', `oadr2b:${cur}`)
+      .import(serializeLoadControlStateType(loadControlState[cur]));
+    return accum;
+  }, fragment());
+  result.ele('oadr2b', 'oadr2b:oadrLoadControlState').import(resultChildren);
+  return result;
+}
+
+function serializePayloadStatus(payloadStatus) {
+  const result = fragment();
+  const payloadResourceStatus = result.ele(
+    'oadr2b',
+    'oadr2b:oadrPayloadResourceStatus',
+  );
+  payloadResourceStatus
+    .ele('oadr2b', 'oadr2b:oadrOnline')
+    .txt(`${payloadStatus.online}`);
+  payloadResourceStatus
+    .ele('oadr2b', 'oadr2b:oadrManualOverride')
+    .txt(`${payloadStatus.manualOverride}`);
+  if (payloadStatus.loadControlState) {
+    payloadResourceStatus.import(
+      serializeLoadControlState(payloadStatus.loadControlState),
+    );
+  }
+
+  return result;
+}
+
+function serializeSignalPayload(signalPayload) {
+  const result = fragment();
+  result
+    .ele(energyInteropNs, 'ei:signalPayload')
+    .import(serializePayloadFloat(signalPayload));
+  return result;
+}
+
+function serializeReportPayload(reportPayload) {
+  const result = fragment();
+  const reportPayloadOut = result.ele(oadrNs, 'oadr2b:oadrReportPayload');
+  if (reportPayload.payloadFloat) {
+    reportPayloadOut.import(serializePayloadFloat(reportPayload.payloadFloat));
+  }
+
+  if (reportPayload.payloadStatus) {
+    reportPayloadOut.import(
+      serializePayloadStatus(reportPayload.payloadStatus),
+    );
+  }
+
+  reportPayloadOut.ele(energyInteropNs, 'ei:rID').txt(reportPayload.reportId);
+  reportPayloadOut
+    .ele(oadrNs, 'oadr2b:oadrDataQuality')
+    .txt(reportPayload.dataQuality);
+  return result;
+}
+
+function parseSignalPayloads(signalPayloads) {
+  return signalPayloads.map(x => parsePayloadFloat(x['$$']));
+}
+
+function parsePayloadStatusType(statusTypeContainer) {
+  return Object.keys(statusTypeContainer).reduce((accum, cur) => {
+    accum[cur] = required(number(statusTypeContainer[cur][0]), cur);
+    return accum;
+  }, {});
+}
+
+function parsePayloadStatus(oadrPayloadResourceStatus) {
+  const result = {
+    online: required(
+      boolean(childAttr(oadrPayloadResourceStatus, 'oadrOnline')),
+      'oadrOnline',
+    ),
+    manualOverride: required(
+      boolean(childAttr(oadrPayloadResourceStatus, 'oadrManualOverride')),
+      'oadrManualOverride',
+    ),
+  };
+  if (oadrPayloadResourceStatus.oadrLoadControlState) {
+    const container = oadrPayloadResourceStatus.oadrLoadControlState[0]['$$'];
+    result.loadControlState = Object.keys(container).reduce((accum, cur) => {
+      accum[cur] = parsePayloadStatusType(container[cur][0]['$$']);
+      return accum;
+    }, {});
+  }
+  return result;
+}
+
+function parseReportPayload(reportPayload) {
+  const result = {
+    reportId: required(childAttr(reportPayload, 'rID'), 'rID'),
+  };
+  if (reportPayload.payloadFloat) {
+    result.payloadFloat = parsePayloadFloat(reportPayload);
+  }
+  if (reportPayload.oadrPayloadResourceStatus) {
+    result.payloadStatus = parsePayloadStatus(
+      reportPayload.oadrPayloadResourceStatus[0]['$$'],
+    );
+  }
+  const dataQuality = childAttr(reportPayload, 'oadrDataQuality');
+  if (dataQuality != null) {
+    result.dataQuality = dataQuality;
+  }
+  return result;
+}
+
+function parseReportPayloads(oadrReportPayloads) {
+  return oadrReportPayloads.map(x => parseReportPayload(x['$$']));
+}
+
+function parseEventSignalInterval(eventSignalInterval) {
+  const result = {};
+
+  if (eventSignalInterval.signalPayload != null) {
+    result.signalPayloads = parseSignalPayloads(
+      eventSignalInterval.signalPayload,
+    );
+  }
+
+  if (eventSignalInterval.oadrReportPayload != null) {
+    result.reportPayloads = parseReportPayloads(
+      eventSignalInterval.oadrReportPayload,
+    );
+  }
+
+  const durationValue = duration(
+    childAttr(eventSignalInterval, 'duration'),
+    'duration',
+  );
+  if (durationValue != null) result.duration = durationValue;
+
+  const dtStartValue = dateTime(
+    childAttr(eventSignalInterval, 'dtstart'),
+    'date-time',
+  );
+  if (dtStartValue != null) {
+    result.startDate = dtStartValue;
+  }
+
+  const uidHolder = childAttr(eventSignalInterval, 'uid');
+  if (uidHolder != null) {
+    result.uid = required(childAttr(uidHolder['$$'], 'text'));
+  }
+
+  return result;
+}
+
+function parseEventSignalIntervals(intervals) {
+  if (intervals.interval != null) {
+    intervals = intervals.interval;
+  }
+  return intervals.map(x => parseEventSignalInterval(x['$$']));
+}
+
+function serializeSamplingRate(samplingRate) {
+  const result = fragment();
+  if (samplingRate == null) return result;
+  result
+    .ele(oadrNs, 'oadr2b:oadrSamplingRate')
+    .ele(oadrNs, 'oadr2b:oadrMinPeriod')
+    .txt(samplingRate.minPeriod)
+    .up()
+    .ele(oadrNs, 'oadr2b:oadrMaxPeriod')
+    .txt(samplingRate.maxPeriod)
+    .up()
+    .ele(oadrNs, 'oadr2b:oadrOnChange')
+    .txt(samplingRate.onChange)
+    .up()
+    .up();
+  return result;
+}
+
+function serializeReportDescription(description) {
+  const result = fragment();
+  const oadrReportDescription = result.ele(
+    oadrNs,
+    'oadr2b:oadrReportDescription',
+  );
+  oadrReportDescription
+    .ele(energyInteropNs, 'ei:rID')
+    .txt(description.reportId)
+    .up()
+    .ele(energyInteropNs, 'ei:reportType')
+    .txt(description.reportType)
+    .up()
+    .ele(energyInteropNs, 'ei:readingType')
+    .txt(description.readingType)
+    .up()
+    .import(serializeSamplingRate(description.samplingRate));
+  return result;
+}
+
+function serializeReportDescriptions(descriptions) {
+  const result = fragment();
+  descriptions.forEach(description =>
+    result.import(serializeReportDescription(description)),
+  );
+  return result;
+}
+
+function serializeReport(report) {
+  const result = fragment();
+  const oadrReport = result.ele(oadrNs, 'oadr2b:oadrReport');
+  oadrReport
+    .ele(energyInteropNs, 'ei:reportRequestID')
+    .txt(report.reportRequestId)
+    .up()
+    .ele(energyInteropNs, 'ei:reportSpecifierID')
+    .txt(report.reportSpecifierId)
+    .up()
+    .ele(energyInteropNs, 'ei:createdDateTime')
+    .txt(report.createdDateTime)
+    .up();
+
+  if (report.reportName != null) {
+    oadrReport.ele(energyInteropNs, 'ei:reportName').txt(report.reportName);
+  }
+
+  if (report.descriptions != null) {
+    oadrReport.import(serializeReportDescriptions(report.descriptions));
+  }
+
+  if (report.duration != null) {
+    oadrReport
+      .ele(calendarNs, 'cal:duration')
+      .import(serializeDuration(report.duration));
+  }
+
+  if (report.startDate != null) {
+    oadrReport
+      .ele(calendarNs, 'cal:dtstart')
+      .import(serializeDateTime(report.startDate));
+  }
+
+  if (report.intervals != null) {
+    const intervals = oadrReport.ele(calendarStreamNs, 'strm:intervals');
+
+    serializeEventSignalIntervals(report.intervals).forEach(interval =>
+      intervals.import(interval),
+    );
+  }
+
+  return result;
+}
+
+function serializeReports(reports) {
+  const result = fragment();
+  reports.forEach(report => {
+    result.import(serializeReport(report));
+  });
+  return result;
+}
+
+function parseDescription(description) {
+  const result = {
+    reportId: required(childAttr(description, 'rID'), 'rID'),
+    reportType: required(childAttr(description, 'reportType'), 'reportType'),
+    readingType: required(childAttr(description, 'readingType'), 'readingType'),
+  };
+
+  if (description['oadrSamplingRate']) {
+    result.samplingRate = parseSamplingRate(
+      description['oadrSamplingRate'][0]['$$'],
+    );
+  }
+  return result;
+}
+
+function parseSamplingRate(samplingRate) {
+  return {
+    minPeriod: required(
+      childAttr(samplingRate, 'oadrMinPeriod'),
+      'oadrMinPeriod',
+    ),
+    maxPeriod: required(
+      childAttr(samplingRate, 'oadrMaxPeriod'),
+      'oadrMaxPeriod',
+    ),
+    onChange: required(
+      boolean(childAttr(samplingRate, 'oadrOnChange'), 'oadrOnChange'),
+    ),
+  };
+}
+
+function parseDescriptions(descriptions) {
+  return descriptions.map(x => parseDescription(x['$$']));
+}
+
+function parseReport(report) {
+  const result = {
+    reportRequestId: required(
+      childAttr(report, 'reportRequestID'),
+      'reportRequestID',
+    ),
+    reportSpecifierId: required(
+      childAttr(report, 'reportSpecifierID'),
+      'reportSpecifierID',
+    ),
+    createdDateTime: required(
+      childAttr(report, 'createdDateTime'),
+      'createdDateTime',
+    ),
+  };
+  const reportName = childAttr(report, 'reportName');
+  if (reportName != null) {
+    result.reportName = reportName;
+  }
+
+  const descriptions = report['oadrReportDescription'];
+  if (descriptions != null) {
+    result.descriptions = parseDescriptions(descriptions);
+  }
+
+  const durationValue = duration(childAttr(report, 'duration'), 'duration');
+  if (durationValue != null) result.duration = durationValue;
+
+  const dtStartValue = dateTime(childAttr(report, 'dtstart'), 'date-time');
+  if (dtStartValue != null) {
+    result.startDate = dtStartValue;
+  }
+
+  const intervalsHolder = childAttr(report, 'intervals');
+  if (intervalsHolder != null && intervalsHolder['$$']) {
+    const intervals = intervalsHolder['$$']['interval'];
+    result.intervals = parseEventSignalIntervals(intervals);
+  }
+  return result;
+}
+
+function parseReports(reports) {
+  return reports.map(x => parseReport(x['$$']));
+}
+
+function serializeEiResponse(data) {
+  const descriptionFrag =
+    data.responseDescription != null
+      ? fragment()
+        .ele(energyInteropNs, 'ei:responseDescription')
+        .txt(data.responseDescription)
+      : fragment();
+
+  return fragment()
+    .ele(energyInteropNs, 'ei:eiResponse')
+    .ele(energyInteropNs, 'ei:responseCode')
+    .txt(data.responseCode)
+    .up()
+    .import(descriptionFrag)
+    .ele(energyInteropPayloadsNs, 'pyld:requestID')
+    .txt(data.responseRequestId)
+    .up()
+    .up();
+}
+
+function serializeReportRequests(reportRequests) {
+  const result = fragment();
+
+  (reportRequests || []).forEach(x => result.import(serializeReportRequest(x)));
+  return result;
+}
+
+function serializeReportRequest(reportRequest) {
+  return fragment()
+    .ele(oadrNs, 'oadr:oadrReportRequest')
+    .ele(energyInteropNs, 'ei:reportRequestID')
+    .txt(reportRequest.reportRequestId)
+    .up()
+    .import(serializeReportSpecifier(reportRequest));
+}
+
+function serializeSpecifierPayloads(specifierPayloads) {
+  return specifierPayloads.map(specifierPayload => {
+    return fragment()
+      .ele(energyInteropNs, 'ei:specifierPayload')
+      .ele(energyInteropNs, 'ei:rID')
+      .txt(specifierPayload.reportId)
+      .up()
+      .ele(energyInteropNs, 'ei:readingType')
+      .txt(specifierPayload.readingType)
+      .up()
+      .up();
+  });
+}
+
+function serializeReportSpecifier(reportRequest) {
+  const result = fragment();
+  const specifier = result.ele(energyInteropNs, 'ei:reportSpecifier');
+
+  specifier
+    .ele(energyInteropNs, 'ei:reportSpecifierID')
+    .txt(reportRequest.reportSpecifierId)
+    .up()
+    .ele(calendarNs, 'cal:granularity')
+    .ele(calendarNs, 'cal:duration')
+    .txt(reportRequest.granularityDuration)
+    .up()
+    .up()
+    .ele(energyInteropNs, 'ei:reportBackDuration')
+    .ele(calendarNs, 'cal:duration')
+    .txt(reportRequest.reportBackDuration)
+    .up()
+    .up()
+    .ele(energyInteropNs, 'ei:reportInterval')
+    .ele(calendarNs, 'cal:properties')
+    .ele(calendarNs, 'cal:dtstart')
+    .import(serializeDateTime(reportRequest.startDate))
+    .up()
+    .ele(calendarNs, 'cal:duration')
+    .import(serializeDuration(reportRequest.duration))
+    .up();
+
+  if (reportRequest.specifiers) {
+    serializeSpecifierPayloads(reportRequest.specifiers).forEach(x =>
+      specifier.import(x),
+    );
+  }
+  return result;
+}
+
+function parseSpecifierPayload(specifierPayload) {
+  return {
+    reportId: required(childAttr(specifierPayload, 'rID'), 'rID'),
+    readingType: required(
+      childAttr(specifierPayload, 'readingType'),
+      'readingType',
+    ),
+  };
+}
+
+function parseSpecifierPayloads(specifierPayloads) {
+  return specifierPayloads.map(x => parseSpecifierPayload(x['$$']));
+}
+
+function parseReportRequest(reportRequest) {
+  const reportSpecifier = reportRequest['reportSpecifier'][0]['$$'];
+  const reportInterval = reportSpecifier['reportInterval'][0]['$$'];
+  const reportIntervalProperties = reportInterval['properties'][0]['$$'];
+
+  const result = {
+    reportRequestId: required(
+      childAttr(reportRequest, 'reportRequestID'),
+      'reportRequestID',
+    ),
+    reportSpecifierId: required(
+      childAttr(reportSpecifier, 'reportSpecifierID'),
+      'reportSpecifierID',
+    ),
+    granularityDuration: required(
+      duration(childAttr(reportSpecifier, 'granularity'), 'duration'),
+      'granularity',
+    ),
+    reportBackDuration: required(
+      duration(childAttr(reportSpecifier, 'reportBackDuration'), 'duration'),
+      'reportBackDuration',
+    ),
+    startDate: required(
+      dateTime(childAttr(reportIntervalProperties, 'dtstart'), 'date-time'),
+      'dtstart',
+    ),
+    duration: required(
+      duration(childAttr(reportIntervalProperties, 'duration'), 'duration'),
+      'duration',
+    ),
+  };
+
+  if (reportSpecifier['specifierPayload']) {
+    result.specifiers = parseSpecifierPayloads(
+      reportSpecifier['specifierPayload'],
+    );
+  }
+  return result;
+}
+
+function parseReportRequests(reportRequests) {
+  return reportRequests.map(x => parseReportRequest(x['$$']));
+}
+
+function parseEiResponse(response) {
+  return {
+    code: required(childAttr(response, 'responseCode'), 'responseCode'),
+    description: childAttr(response, 'responseDescription'),
+    requestId: required(childAttr(response, 'requestID'), 'requestID'),
+  };
+}
+
+function createDoc() {
+  return create({
+    namespaceAlias: {
+      ns: oadrPayloadNs,
+      oadr2b: oadrNs,
+      ei: energyInteropNs,
+      pyld: energyInteropPayloadsNs,
+      cal: calendarNs,
+      strm: calendarStreamNs,
+    },
+  });
+}
+
+module.exports = {
+  createDoc,
+  oadrPayloadNs,
+  oadrNs,
+  energyInteropNs,
+  energyInteropPayloadsNs,
+  calendarNs,
+  calendarStreamNs,
+  xsiNs,
+  emixNs,
+  powerNs,
+  siScaleNs,
+  serializeDateTime,
+  serializeDuration,
+  parsePayloadFloat,
+  serializePayloadFloat,
+  serializeEventSignalIntervals,
+  parseEventSignalIntervals,
+  serializeReports,
+  parseReports,
+  serializeEiResponse,
+  parseEiResponse,
+  serializeReportRequests,
+  parseReportRequests,
+};