Browse Source

Refinements to Reporting for Client

Stan Janssen 5 years ago
parent
commit
c488227417

+ 122 - 4
pyopenadr/client.py

@@ -4,12 +4,21 @@ import xmltodict
 import random
 import random
 import requests
 import requests
 from jinja2 import Environment, PackageLoader, select_autoescape
 from jinja2 import Environment, PackageLoader, select_autoescape
-from pyopenadr.utils import parse_message, create_message, new_request_id, peek
+from pyopenadr.utils import parse_message, create_message, new_request_id, peek, generate_id
+from pyopenadr import enums
+from datetime import datetime, timedelta, timezone
 from http import HTTPStatus
 from http import HTTPStatus
 from apscheduler.schedulers.asyncio import AsyncIOScheduler
 from apscheduler.schedulers.asyncio import AsyncIOScheduler
 import asyncio
 import asyncio
 from asyncio import iscoroutine
 from asyncio import iscoroutine
 
 
+MEASURANDS = {'power_real': 'power_quantity',
+              'power_reactive': 'power_quantity',
+              'power_apparent': 'power_quantity',
+              'energy_real': 'energy_quantity',
+              'energy_reactive': 'energy_quantity',
+              'energy_active': 'energy_quantity'}
+
 class OpenADRClient:
 class OpenADRClient:
     def __init__(self, ven_name, vtn_url, debug=False):
     def __init__(self, ven_name, vtn_url, debug=False):
         self.ven_name = ven_name
         self.ven_name = ven_name
@@ -17,6 +26,10 @@ class OpenADRClient:
         self.ven_id = None
         self.ven_id = None
         self.poll_frequency = None
         self.poll_frequency = None
         self.debug = debug
         self.debug = debug
+        self.reports = {}           # Mapping of all available reports from the VEN
+        self.report_requests = {}   # Mapping of the reports requested by the VTN
+        self.report_schedulers = {} # Mapping between reportRequestIDs and our internal report schedulers
+        self.scheduler = AsyncIOScheduler()
 
 
     def run(self):
     def run(self):
         """
         """
@@ -31,8 +44,10 @@ class OpenADRClient:
             print("No VEN ID received from the VTN, aborting registration.")
             print("No VEN ID received from the VTN, aborting registration.")
             return
             return
 
 
+        if self.reports:
+            self.register_report()
+
         # Set up automatic polling
         # Set up automatic polling
-        self.scheduler = AsyncIOScheduler()
         if self.poll_frequency.total_seconds() < 60:
         if self.poll_frequency.total_seconds() < 60:
             cron_second = f"*/{self.poll_frequency.seconds}"
             cron_second = f"*/{self.poll_frequency.seconds}"
             cron_minute = "*"
             cron_minute = "*"
@@ -52,6 +67,57 @@ class OpenADRClient:
         self.scheduler.add_job(self._poll, trigger='cron', second=cron_second, minute=cron_minute, hour=cron_hour)
         self.scheduler.add_job(self._poll, trigger='cron', second=cron_second, minute=cron_minute, hour=cron_hour)
         self.scheduler.start()
         self.scheduler.start()
 
 
+    def add_report(self, callable, report_id, report_name, reading_type, report_type,
+                         sampling_rate, resource_id, measurand, unit, scale="none",
+                         power_ac=True, power_hertz=50, power_voltage=230, market_context=None):
+        """
+        Add a new reporting capability to the client.
+        :param callable callable: A callable or coroutine that will fetch the value for a specific report. This callable will be passed the report_id and the r_id of the requested value.
+        :param str report_id: A unique identifier for this report.
+        :param str report_name: An OpenADR name for this report (one of pyopenadr.enums.REPORT_NAME)
+        :param str reading_type: An OpenADR reading type (found in pyopenadr.enums.READING_TYPE)
+        :param str report_type: An OpenADR report type (found in pyopenadr.enums.REPORT_TYPE)
+        :param datetime.timedelta sampling_rate: The sampling rate for the measurement.
+        :param resource_id: A specific name for this resource within this report.
+        :param str unit: The unit for this measurement.
+        """
+
+        if report_name not in enums.REPORT_NAME.values:
+            raise ValueError(f"{report_name} is not a valid report_name. Valid options are {', '.join(enums.REPORT_NAME.values)}.")
+        if reading_type not in enums.READING_TYPE.values:
+            raise ValueError(f"{reading_type} is not a valid reading_type. Valid options are {', '.join(enums.READING_TYPE.values)}.")
+        if report_type not in enums.REPORT_TYPE.values:
+            raise ValueError(f"{report_type} is not a valid report_type. Valid options are {', '.join(enums.REPORT_TYPE.values)}.")
+        if measurand not in MEASURANDS:
+            raise ValueError(f"{measurand} is not a valid measurand. Valid options are 'power_real', 'power_reactive', 'power_apparent', 'energy_real', 'energy_reactive', 'energy_active', 'energy_quantity', 'voltage'")
+        if scale not in enums.SI_SCALE_CODE.values:
+            raise ValueError(f"{scale} is not a valid scale. Valid options are {', '.join(enums.SI_SCALE_CODE.values)}")
+
+        report_description = {'market_context': market_context,
+                              'r_id': resource_id,
+                              'reading_type': reading_type,
+                              'report_type': report_type,
+                              'sampling_rate': {'max_period': sampling_rate,
+                                                'min_period': sampling_rate,
+                                                'on_change': False},
+                               measurand: {'item_description': measurand,
+                                           'item_units': unit,
+                                           'si_scale_code': scale}}
+        if 'power' in measurand:
+            report_description[measurand]['power_attributes'] = {'ac': power_ac, 'hertz': power_hertz, 'voltage': power_voltage}
+
+        if report_id in self.reports:
+            report = self.reports[report_id]['report_descriptions'].append(report_description)
+        else:
+            report = {'callable': callable,
+                      'created_date_time': datetime.now(timezone.utc),
+                      'report_id': report_id,
+                      'report_name': report_name,
+                      'report_request_id': generate_id(),
+                      'report_specifier_id': report_id + "_" + report_name.lower(),
+                      'report_descriptions': [report_description]}
+        self.reports[report_id] = report
+        self.report_ids[resource_id] = {'item_base': measurand}
 
 
     def query_registration(self):
     def query_registration(self):
         """
         """
@@ -105,7 +171,6 @@ class OpenADRClient:
         response_type, response_payload = self._perform_request(service, message)
         response_type, response_payload = self._perform_request(service, message)
         return response_type, response_payload
         return response_type, response_payload
 
 
-
     def created_event(self, request_id, event_id, opt_type, modification_number=1):
     def created_event(self, request_id, event_id, opt_type, modification_number=1):
         """
         """
         Inform the VTN that we created an event.
         Inform the VTN that we created an event.
@@ -129,7 +194,22 @@ class OpenADRClient:
         """
         """
         Tell the VTN about our reporting capabilities.
         Tell the VTN about our reporting capabilities.
         """
         """
-        raise NotImplementedError("Reporting is not yet implemented")
+        request_id = generate_id()
+
+        payload = {'request_id': generate_id(),
+                   'ven_id': self.ven_id,
+                   'reports': self.reports}
+
+        service = 'EiReport'
+        message = create_message('oadrRegisterReport', **payload)
+        response_type, response_payload = self._perform_request(service, message)
+
+        # Remember which reports the VTN is interested in
+
+        return response_type, response_payload
+
+    def created_report(self):
+        pass
 
 
     def poll(self):
     def poll(self):
         service = 'OadrPoll'
         service = 'OadrPoll'
@@ -137,6 +217,44 @@ class OpenADRClient:
         response_type, response_payload = self._perform_request(service, message)
         response_type, response_payload = self._perform_request(service, message)
         return response_type, response_payload
         return response_type, response_payload
 
 
+    async def update_report(self, report_id, resource_id=None):
+        """
+        Calls the previously registered report callable, and send the result as a message to the VTN.
+        """
+        if not resource_id:
+            resource_ids = self.reports[report_id]['report_descriptions'].keys()
+        elif isinstance(resource_id, str):
+            resource_ids = [resource_id]
+        else:
+            resource_ids = resource_id
+        value = self.reports[report_id]['callable'](resource_id)
+        if iscoroutine(value):
+            value = await value
+
+        report_type = self.reports[report_id][resource_id]['report_type']
+        for measurand in MEASURAND:
+            if measurand in self.reports[report_id][resource_id]:
+                item_base = measurand
+                break
+        report = {'report_id': report_id,
+                  'report_descriptions': {resource_id: {MEASURANDS[measurand]: {'quantity': value,
+                                                                         measurand: self.reports[report_id][resource_id][measurand]},
+                                          'report_type': self.reports[report_id][resource_id]['report_type'],
+                                          'reading_type': self.reports[report_id][resource_id]['reading_type']}},
+                  'report_name': self.report['report_id']['report_name'],
+                  'report_request_id': self.reports['report_id']['report_request_id'],
+                  'report_specifier_id': self.report['report_id']['report_specifier_id'],
+                  'created_date_time': datetime.now(timezone.utc)}
+
+        service = 'EiReport'
+        message = create_message('oadrUpdateReport', report)
+        response_type, response_payload = self._perform_request(service, message)
+
+        # We might get a oadrCancelReport message in this thing:
+        if 'cancel_report' in response.payload:
+            print("TODO: cancel this report")
+
+
     def _perform_request(self, service, message):
     def _perform_request(self, service, message):
         if self.debug:
         if self.debug:
             print(f"Sending {message}")
             print(f"Sending {message}")

+ 16 - 3
pyopenadr/enums.py

@@ -8,7 +8,7 @@ class Enum(type):
 
 
     @property
     @property
     def values(self):
     def values(self):
-        return (self[item] for item in self.members)
+        return [self[item] for item in self.members]
 
 
 class EVENT_STATUS(metaclass=Enum):
 class EVENT_STATUS(metaclass=Enum):
     NONE = "none"
     NONE = "none"
@@ -44,11 +44,24 @@ class SIGNAL_NAME(metaclass=Enum):
     LOAD_DISPATCH = "LOAD_DISPATCH"
     LOAD_DISPATCH = "LOAD_DISPATCH"
     LOAD_CONTROL = "LOAD_CONTROL"
     LOAD_CONTROL = "LOAD_CONTROL"
 
 
+class SI_SCALE_CODE(metaclass=Enum):
+    p = "p"
+    n = "n"
+    micro = "micro"
+    m = "m"
+    c = "c"
+    d = "d"
+    k = "k"
+    M = "M"
+    G = "G"
+    T = "T"
+    none = "none"
+
 class OPT(metaclass=Enum):
 class OPT(metaclass=Enum):
     OPT_IN = "optIn"
     OPT_IN = "optIn"
     OPT_OUT = "optOut"
     OPT_OUT = "optOut"
 
 
-class OPT_REASON(metaclass=Enum)
+class OPT_REASON(metaclass=Enum):
     ECONOMIC = "economic"
     ECONOMIC = "economic"
     EMERGENCY = "emergency"
     EMERGENCY = "emergency"
     MUST_RUN = "mustRun"
     MUST_RUN = "mustRun"
@@ -101,7 +114,7 @@ class REPORT_TYPE(metaclass=Enum):
 
 
 class REPORT_NAME(metaclass=Enum):
 class REPORT_NAME(metaclass=Enum):
     METADATA_HISTORY_USAGE = "METADATA_HISTORY_USAGE"
     METADATA_HISTORY_USAGE = "METADATA_HISTORY_USAGE"
-    HISTORY_USAGE = "METADATA_HISTORY_USAGE"
+    HISTORY_USAGE = "HISTORY_USAGE"
     METADATA_HISTORY_GREENBUTTON = "METADATA_HISTORY_GREENBUTTON"
     METADATA_HISTORY_GREENBUTTON = "METADATA_HISTORY_GREENBUTTON"
     HISTORY_GREENBUTTON = "HISTORY_GREENBUTTON"
     HISTORY_GREENBUTTON = "HISTORY_GREENBUTTON"
     METADATA_TELEMETRY_USAGE = "METADATA_TELEMETRY_USAGE"
     METADATA_TELEMETRY_USAGE = "METADATA_TELEMETRY_USAGE"

+ 1 - 0
pyopenadr/server.py

@@ -16,6 +16,7 @@ class OpenADRServer:
         map = {'on_created_event': EventService,
         map = {'on_created_event': EventService,
                'on_request_event': EventService,
                'on_request_event': EventService,
 
 
+               'on_register_report': ReportService,
                'on_create_report': ReportService,
                'on_create_report': ReportService,
                'on_created_report': ReportService,
                'on_created_report': ReportService,
                'on_request_report': ReportService,
                'on_request_report': ReportService,

+ 2 - 2
pyopenadr/templates/oadrRegisterReport.xml

@@ -12,11 +12,11 @@
         {% endif %}
         {% endif %}
         {% if report.duration %}
         {% if report.duration %}
         <xcal:duration>
         <xcal:duration>
-          <duration>{{ report.duration|timedeltaformat }}</duration>
+          <xcal:duration>{{ report.duration|timedeltaformat }}</xcal:duration>
         </xcal:duration>
         </xcal:duration>
         {% endif %}
         {% endif %}
         <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
         <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
-    {% for report_description in report.report_descriptions %}
+    {% for r_id, report_description in report.report_descriptions.items() %}
         {% include 'parts/oadrReportDescription.xml' %}
         {% include 'parts/oadrReportDescription.xml' %}
     {% endfor %}
     {% endfor %}
         <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>
         <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>

+ 1 - 1
pyopenadr/templates/oadrUpdateReport.xml

@@ -8,7 +8,7 @@
       <oadrReport>
       <oadrReport>
         <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
         <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
         {% if report.report_descriptions %}
         {% if report.report_descriptions %}
-        {% for report_description in report.report_descriptions %}
+        {% for r_id, report_description in report.report_descriptions.items() %}
         {% include 'parts/oadrReportDescription.xml' %}
         {% include 'parts/oadrReportDescription.xml' %}
         <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>
         <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>
         <ei:reportSpecifierID>{{ report.report_specifier_id }}</ei:reportSpecifierID>
         <ei:reportSpecifierID>{{ report.report_specifier_id }}</ei:reportSpecifierID>

+ 1 - 1
pyopenadr/templates/parts/oadrReportDescription.xml

@@ -1,5 +1,5 @@
 <oadrReportDescription xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06">
 <oadrReportDescription xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06">
-  <ei:rID>{{ report_description.r_id }}</ei:rID>
+  <ei:rID>{{ r_id }}</ei:rID>
   {% if report_description.report_subjects %}
   {% if report_description.report_subjects %}
   <ei:reportSubject>
   <ei:reportSubject>
   {% for target in report_description.report_subjects %}
   {% for target in report_description.report_subjects %}

+ 20 - 6
pyopenadr/utils.py

@@ -134,11 +134,12 @@ def normalize_dict(ordered_dict):
         if key in ("target", "report_subject", "report_data_source"):
         if key in ("target", "report_subject", "report_data_source"):
             targets = d.pop(key)
             targets = d.pop(key)
             new_targets = []
             new_targets = []
-            for ikey in targets:
-                if isinstance(targets[ikey], list):
-                    new_targets.extend([{ikey: value} for value in targets[ikey]])
-                else:
-                    new_targets.append({ikey: targets[ikey]})
+            if targets:
+                for ikey in targets:
+                    if isinstance(targets[ikey], list):
+                        new_targets.extend([{ikey: value} for value in targets[ikey]])
+                    else:
+                        new_targets.append({ikey: targets[ikey]})
             d[key + "s"] = new_targets
             d[key + "s"] = new_targets
             key = key + "s"
             key = key + "s"
 
 
@@ -168,12 +169,25 @@ def normalize_dict(ordered_dict):
             d = d[key]
             d = d[key]
 
 
         # Plurarize some lists
         # Plurarize some lists
-        elif key in ('report_request', 'report_description', 'report'):
+        elif key in ('report_request', 'report'):
             if isinstance(d[key], list):
             if isinstance(d[key], list):
                 d[key + 's'] = d.pop(key)
                 d[key + 's'] = d.pop(key)
             else:
             else:
                 d[key + 's'] = [d.pop(key)]
                 d[key + 's'] = [d.pop(key)]
 
 
+        elif key == 'report_description':
+            if isinstance(d[key], list):
+                original_descriptions = d.pop(key)
+                report_descriptions = {}
+                for item in original_descriptions:
+                    r_id = item.pop('r_id')
+                    report_descriptions[r_id] = item
+                d[key + 's'] = report_descriptions
+            else:
+                original_description = d.pop(key)
+                r_id = original_description.pop('r_id')
+                d[key + 's'] = {r_id: original_description}
+
         # Promote the contents of the Qualified Event ID
         # Promote the contents of the Qualified Event ID
         elif key == "qualified_event_id" and isinstance(d['qualified_event_id'], dict):
         elif key == "qualified_event_id" and isinstance(d['qualified_event_id'], dict):
             qeid = d.pop('qualified_event_id')
             qeid = d.pop('qualified_event_id')

+ 19 - 20
test/test_message_conversion.py

@@ -141,14 +141,14 @@ test_message('oadrRegisteredReport', ven_id='VEN123', response={'response_code':
 test_message('oadrRequestEvent', request_id=generate_id(), ven_id='123ABC')
 test_message('oadrRequestEvent', request_id=generate_id(), ven_id='123ABC')
 test_message('oadrRequestReregistration', ven_id='123ABC')
 test_message('oadrRequestReregistration', ven_id='123ABC')
 test_message('oadrRegisterReport', request_id=generate_id(), reports=[{'report_id': generate_id(),
 test_message('oadrRegisterReport', request_id=generate_id(), reports=[{'report_id': generate_id(),
-                                                                       'report_descriptions': [{
-                                                                            'r_id': generate_id(),
+                                                                       'report_descriptions': {
+                                                                            generate_id(): {
                                                                             'report_subjects': [{'ven_id': '123ABC'}],
                                                                             'report_subjects': [{'ven_id': '123ABC'}],
                                                                             'report_data_sources': [{'ven_id': '123ABC'}],
                                                                             'report_data_sources': [{'ven_id': '123ABC'}],
                                                                             'report_type': 'reading',
                                                                             'report_type': 'reading',
                                                                             'reading_type': 'Direct Read',
                                                                             'reading_type': 'Direct Read',
                                                                             'market_context': 'http://localhost',
                                                                             'market_context': 'http://localhost',
-                                                                            'sampling_rate': {'min_period': timedelta(minutes=1), 'max_period': timedelta(minutes=1), 'on_change': True}}],
+                                                                            'sampling_rate': {'min_period': timedelta(minutes=1), 'max_period': timedelta(minutes=1), 'on_change': True}}},
                                                                        'report_request_id': generate_id(),
                                                                        'report_request_id': generate_id(),
                                                                        'report_specifier_id': generate_id(),
                                                                        'report_specifier_id': generate_id(),
                                                                        'report_name': 'HISTORY_USAGE',
                                                                        'report_name': 'HISTORY_USAGE',
@@ -157,19 +157,19 @@ test_message('oadrRegisterReport', request_id=generate_id(), reports=[{'report_i
                                                         report_request_id=generate_id())
                                                         report_request_id=generate_id())
 test_message('oadrRegisterReport', **{'request_id': '8a4f859883', 'reports': [{'report_id': generate_id(),
 test_message('oadrRegisterReport', **{'request_id': '8a4f859883', 'reports': [{'report_id': generate_id(),
                                                                                'duration': timedelta(seconds=7200),
                                                                                'duration': timedelta(seconds=7200),
-                                                                               'report_descriptions': [{'r_id': 'resource1_status',
+                                                                               'report_descriptions': {'resource1_status': {
                                                                                                         'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                         'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                         'report_type': 'x-resourceStatus',
                                                                                                         'report_type': 'x-resourceStatus',
                                                                                                         'reading_type': 'x-notApplicable',
                                                                                                         'reading_type': 'x-notApplicable',
                                                                                                         'market_context': 'http://MarketContext1',
                                                                                                         'market_context': 'http://MarketContext1',
-                                                                                                        'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}],
+                                                                                                        'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
                                                                                 'report_request_id': '0',
                                                                                 'report_request_id': '0',
                                                                                 'report_specifier_id': '789ed6cd4e_telemetry_status',
                                                                                 'report_specifier_id': '789ed6cd4e_telemetry_status',
                                                                                 'report_name': 'METADATA_TELEMETRY_STATUS',
                                                                                 'report_name': 'METADATA_TELEMETRY_STATUS',
                                                                                 'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
                                                                                 'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
                                                                                {'report_id': generate_id(),
                                                                                {'report_id': generate_id(),
                                                                                 'duration': timedelta(seconds=7200),
                                                                                 'duration': timedelta(seconds=7200),
-                                                                                'report_descriptions': [{'r_id': 'resource1_energy',
+                                                                                'report_descriptions': {'resource1_energy': {
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_type': 'usage',
                                                                                                          'report_type': 'usage',
                                                                                                          'energy_real': {'item_description': 'RealEnergy',
                                                                                                          'energy_real': {'item_description': 'RealEnergy',
@@ -178,7 +178,7 @@ test_message('oadrRegisterReport', **{'request_id': '8a4f859883', 'reports': [{'
                                                                                                          'reading_type': 'Direct Read',
                                                                                                          'reading_type': 'Direct Read',
                                                                                                          'market_context': 'http://MarketContext1',
                                                                                                          'market_context': 'http://MarketContext1',
                                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
                                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
-                                                                                                        {'r_id': 'resource1_power',
+                                                                                                        'resource1_power': {
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_type': 'usage',
                                                                                                          'report_type': 'usage',
                                                                                                          'power_real': {'item_description': 'RealPower',
                                                                                                          'power_real': {'item_description': 'RealPower',
@@ -187,14 +187,14 @@ test_message('oadrRegisterReport', **{'request_id': '8a4f859883', 'reports': [{'
                                                                                                                         'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
                                                                                                                         'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
                                                                                                           'reading_type': 'Direct Read',
                                                                                                           'reading_type': 'Direct Read',
                                                                                                           'market_context': 'http://MarketContext1',
                                                                                                           'market_context': 'http://MarketContext1',
-                                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}],
+                                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
                                                                                 'report_request_id': '0',
                                                                                 'report_request_id': '0',
                                                                                 'report_specifier_id': '789ed6cd4e_telemetry_usage',
                                                                                 'report_specifier_id': '789ed6cd4e_telemetry_usage',
                                                                                 'report_name': 'METADATA_TELEMETRY_USAGE',
                                                                                 'report_name': 'METADATA_TELEMETRY_USAGE',
                                                                                 'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
                                                                                 'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
                                                                                {'report_id': generate_id(),
                                                                                {'report_id': generate_id(),
                                                                                 'duration': timedelta(seconds=7200),
                                                                                 'duration': timedelta(seconds=7200),
-                                                                                'report_descriptions': [{'r_id': 'resource1_energy',
+                                                                                'report_descriptions': {'resource1_energy': {
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_type': 'usage',
                                                                                                          'report_type': 'usage',
                                                                                                          'energy_real': {'item_description': 'RealEnergy',
                                                                                                          'energy_real': {'item_description': 'RealEnergy',
@@ -203,7 +203,7 @@ test_message('oadrRegisterReport', **{'request_id': '8a4f859883', 'reports': [{'
                                                                                                          'reading_type': 'Direct Read',
                                                                                                          'reading_type': 'Direct Read',
                                                                                                          'market_context': 'http://MarketContext1',
                                                                                                          'market_context': 'http://MarketContext1',
                                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
                                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
-                                                                                                        {'r_id': 'resource1_power',
+                                                                                                        'resource1_power': {
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
                                                                                                          'report_type': 'usage',
                                                                                                          'report_type': 'usage',
                                                                                                          'power_real': {'item_description': 'RealPower',
                                                                                                          'power_real': {'item_description': 'RealPower',
@@ -211,7 +211,7 @@ test_message('oadrRegisterReport', **{'request_id': '8a4f859883', 'reports': [{'
                                                                                                                         'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
                                                                                                                         'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
                                                                                                          'reading_type': 'Direct Read',
                                                                                                          'reading_type': 'Direct Read',
                                                                                                          'market_context': 'http://MarketContext1',
                                                                                                          'market_context': 'http://MarketContext1',
-                                                                                                         'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}],
+                                                                                                         'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
                                                                                 'report_request_id': '0',
                                                                                 'report_request_id': '0',
                                                                                 'report_specifier_id': '789ed6cd4e_history_usage',
                                                                                 'report_specifier_id': '789ed6cd4e_history_usage',
                                                                                 'report_name': 'METADATA_HISTORY_USAGE',
                                                                                 'report_name': 'METADATA_HISTORY_USAGE',
@@ -224,15 +224,14 @@ test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id'
                                                                                   'created_date_time': datetime.now(timezone.utc),
                                                                                   'created_date_time': datetime.now(timezone.utc),
                                                                                   'report_request_id': generate_id(),
                                                                                   'report_request_id': generate_id(),
                                                                                   'report_specifier_id': generate_id(),
                                                                                   'report_specifier_id': generate_id(),
-                                                                                 'report_descriptions': [{'r_id': generate_id(),
-                                                                                                          'report_subjects': [{'ven_id': '123ABC'}, {'ven_id': 'DEF456'}],
-                                                                                                          'report_data_sources': [{'ven_id': '123ABC'}],
-                                                                                                          'report_type': enums.REPORT_TYPE.values[0],
-                                                                                                          'reading_type': enums.READING_TYPE.values[0],
-                                                                                                          'market_context': 'http://localhost',
-                                                                                                          'sampling_rate': {'min_period': timedelta(minutes=1),
-                                                                                                                            'max_period': timedelta(minutes=2),
-                                                                                                                            'on_change': False}}]}], ven_id='123ABC')
+                                                                                  'report_descriptions': {generate_id(): {'report_subjects': [{'ven_id': '123ABC'}, {'ven_id': 'DEF456'}],
+                                                                                                                          'report_data_sources': [{'ven_id': '123ABC'}],
+                                                                                                                          'report_type': enums.REPORT_TYPE.values[0],
+                                                                                                                          'reading_type': enums.READING_TYPE.values[0],
+                                                                                                                          'market_context': 'http://localhost',
+                                                                                                                          'sampling_rate': {'min_period': timedelta(minutes=1),
+                                                                                                                                            'max_period': timedelta(minutes=2),
+                                                                                                                                            'on_change': False}}}}], ven_id='123ABC')
 # for report_name in enums.REPORT_NAME.values:
 # for report_name in enums.REPORT_NAME.values:
 #     for reading_type in enums.READING_TYPE.values:
 #     for reading_type in enums.READING_TYPE.values:
 #         for report_type in enums.REPORT_TYPE.values:
 #         for report_type in enums.REPORT_TYPE.values: