Quellcode durchsuchen

Restructured the Reports logic

Signed-off-by: Stan Janssen <stan.janssen@elaad.nl>
Stan Janssen vor 3 Jahren
Ursprung
Commit
bf31787a9d
5 geänderte Dateien mit 134 neuen und 71 gelöschten Zeilen
  1. 25 0
      openleadr/enums.py
  2. 4 1
      openleadr/objects.py
  3. 74 59
      openleadr/service/report_service.py
  4. 6 6
      openleadr/utils.py
  5. 25 5
      test/test_reports.py

+ 25 - 0
openleadr/enums.py

@@ -145,6 +145,31 @@ class REPORT_TYPE(metaclass=Enum):
     X_RESOURCE_STATUS = "x-resourceStatus"
 
 
+class SIGNAL_TARGET_MRID(metaclass=Enum):
+    THERMOSTAT = "Thermostat"
+    STRIP_HEATER = "Strip_Heater"
+    BASEBOARD_HEATER = "Baseboard_Heater"
+    WATER_HEATER = "Water_Heater"
+    POOL_PUMP = "Pool_Pump"
+    SAUNA = "Sauna"
+    HOT_TUB = "Hot_tub"
+    SMART_APPLIANCE = "Smart_Appliance"
+    IRRIGATION_PUMP = "Irrigation_Pump"
+    MANAGED_COMMERCIAL_AND_INDUSTRIAL_LOADS = "Managed_Commercial_and_Industrial_Loads"
+    SIMPLE_RESIDENTIAL_ON_OFF_LOADS = "Simple_Residential_On_Off_Loads"
+    EXTERIOR_LIGHTING = "Exterior_Lighting"
+    INTERIOR_LIGHTING = "Interior_Lighting"
+    ELECTRIC_VEHICLE = "Electric_Vehicle"
+    GENERATION_SYSTEMS = "Generation_Systems"
+    LOAD_CONTROL_SWITCH = "Load_Control_Switch"
+    SMART_INVERTER = "Smart_Inverter"
+    EVSE = "EVSE"
+    RESU = "RESU"
+    ENERGY_MANAGEMENT_SYSTEM = "Energy_Management_System"
+    SMART_ENERGY_MODULE = "Smart_Energy_Module"
+    STORAGE = "Storage"
+
+
 class REPORT_NAME(metaclass=Enum):
     METADATA_HISTORY_USAGE = "METADATA_HISTORY_USAGE"
     HISTORY_USAGE = "HISTORY_USAGE"

+ 4 - 1
openleadr/objects.py

@@ -177,7 +177,10 @@ class EventSignal:
             return
         elif self.targets_by_type is None:
             list_of_targets = [asdict(target) if is_dataclass(target) else target for target in self.targets]
-            self.targets_by_type = utils.group_targets_by_type(list_of_targets)
+            targets_by_type = utils.group_targets_by_type(list_of_targets)
+            if len(targets_by_type) > 1:
+                raise ValueError("In OpenADR, the EventSignal target may only be of type endDeviceAsset. "
+                                 f"You provided types: {', '.join(targets_by_type)}")
         elif self.targets is None:
             self.targets = [Target(**target) for target in utils.ungroup_targets_by_type(self.targets_by_type)]
         elif self.targets is not None and self.targets_by_type is not None:

+ 74 - 59
openleadr/service/report_service.py

@@ -15,7 +15,7 @@
 # limitations under the License.
 
 from . import service, handler, VTNService
-from asyncio import iscoroutine, gather
+from asyncio import iscoroutine
 from openleadr import objects, utils
 import logging
 import inspect
@@ -63,10 +63,9 @@ logger = logging.getLogger('openleadr')
 @service('EiReport')
 class ReportService(VTNService):
 
-    def __init__(self, vtn_id, message_queues=None):
+    def __init__(self, vtn_id):
         super().__init__(vtn_id)
         self.report_callbacks = {}
-        self.message_queues = message_queues
         self.registered_reports = {}
 
     @handler('oadrRegisterReport')
@@ -79,63 +78,79 @@ class ReportService(VTNService):
         if all(['ven_id' in args, 'resource_id' in args, 'measurement' in args,
                 'min_sampling_interval' in args, 'max_sampling_interval' in args,
                 'unit' in args, 'scale' in args]):
-            for report in payload['reports']:
-                if report['report_name'] == 'METADATA_TELEMETRY_STATUS':
-                    result = [self.on_register_report(ven_id=payload['ven_id'],
-                                                      resource_id=rd.get('report_data_source', {}).get('resource_id'),
-                                                      measurement='Status',
-                                                      unit=None,
-                                                      scale=None,
-                                                      min_sampling_interval=rd['sampling_rate']['min_period'],
-                                                      max_sampling_interval=rd['sampling_rate']['max_period'])
-                              for rd in report['report_descriptions']]
-                elif report['report_name'] == 'METADATA_TELEMETRY_USAGE':
-                    result = [self.on_register_report(ven_id=payload['ven_id'],
-                                                      resource_id=rd.get('report_data_source', {}).get('resource_id'),
-                                                      measurement=rd['measurement']['description'],
-                                                      unit=rd['measurement']['unit'],
-                                                      scale=rd['measurement']['scale'],
-                                                      min_sampling_interval=rd['sampling_rate']['min_period'],
-                                                      max_sampling_interval=rd['sampling_rate']['max_period'])
-                              for rd in report['report_descriptions']]
-                elif report['report_name'] in ('METADATA_HISTORY_USAGE', 'METADATA_HISTORY_GREENBUTTON'):
-                    if payload['ven_id'] not in self.available_reports:
-                        self.available_reports[payload['ven_id']] = []
-                    self.registered_reports[payload['ven_id']].append(report)
-                else:
-                    logger.warning("Reports other than TELEMETRY_USAGE, TELEMETRY_STATUS, "
-                                   "HISTORY_USAGE and HISTORY_GREENBUTTON are not yet supported. "
-                                   f"Skipping report with name {report['report_name']}.")
-                    report_requests.append(None)
-                    continue
-
-                if iscoroutine(result[0]):
-                    result = await gather(*result)
-                for i, r in enumerate(result):
-                    if r is None:
-                        continue
-                    if not isinstance(r, tuple):
-                        logger.error("Your on_register_report handler must return a tuple; "
-                                     f"it returned '{r}' ({r.__class__.__name__}).")
-                        result[i] = None
-                result = [(report['report_descriptions'][i]['r_id'], *result[i])
-                          for i in range(len(report['report_descriptions'])) if isinstance(result[i], tuple)]
-                report_requests.append(result)
-            utils.validate_report_request_tuples(report_requests)
+            mode = 'compact'
         else:
-            # Use the 'full' mode for openADR reporting
-            result = [self.on_register_report(report) for report in payload['reports']]
-            if iscoroutine(result[0]):
-                result = await gather(*result)      # Now we have r_id, callback, sampling_rate
-            for i, r in enumerate(result):
-                if r is None:
-                    continue
-                if not isinstance(r, list):
-                    logger.error("Your on_register_report handler must return a list of tuples. "
-                                 f"It returned '{r}' ({r.__class__.__name__}).")
-                    result[i] = None
-            report_requests = result
-            utils.validate_report_request_tuples(report_requests, full_mode=True)
+            mode = 'full'
+
+        if payload['reports'] is None:
+            return
+
+        for report in payload['reports']:
+            if report['report_name'] == 'METADATA_TELEMETRY_STATUS':
+                if mode == 'compact':
+                    results = [self.on_register_report(ven_id=payload['ven_id'],
+                                                       resource_id=rd.get('report_data_source', {}).get('resource_id'),
+                                                       measurement='Status',
+                                                       unit=None,
+                                                       scale=None,
+                                                       min_sampling_interval=rd['sampling_rate']['min_period'],
+                                                       max_sampling_interval=rd['sampling_rate']['max_period'])
+                               for rd in report['report_descriptions']]
+                    results = await utils.gather_if_required(results)
+                elif mode == 'full':
+                    results = await utils.await_if_required(self.on_register_report(report))
+            elif report['report_name'] == 'METADATA_TELEMETRY_USAGE':
+                if mode == 'compact':
+                    results = [self.on_register_report(ven_id=payload['ven_id'],
+                                                       resource_id=rd.get('report_data_source', {}).get('resource_id'),
+                                                       measurement=rd['measurement']['description'],
+                                                       unit=rd['measurement']['unit'],
+                                                       scale=rd['measurement']['scale'],
+                                                       min_sampling_interval=rd['sampling_rate']['min_period'],
+                                                       max_sampling_interval=rd['sampling_rate']['max_period'])
+                               for rd in report['report_descriptions']]
+                    results = await utils.gather_if_required(results)
+                elif mode == 'full':
+                    results = await utils.await_if_required(self.on_register_report(report))
+            elif report['report_name'] in ('METADATA_HISTORY_USAGE', 'METADATA_HISTORY_GREENBUTTON'):
+                if payload['ven_id'] not in self.registered_reports:
+                    self.registered_reports[payload['ven_id']] = []
+                report['report_name'] = report['report_name'][9:]
+                self.registered_reports[payload['ven_id']].append(report)
+                report_requests.append(None)
+                continue
+            else:
+                logger.warning("Reports other than TELEMETRY_USAGE, TELEMETRY_STATUS, "
+                               "HISTORY_USAGE and HISTORY_GREENBUTTON are not yet supported. "
+                               f"Skipping report with name {report['report_name']}.")
+                report_requests.append(None)
+                continue
+
+            # Perform some rudimentary checks on the returned type
+            if results is not None:
+                if not isinstance(results, list):
+                    logger.error("Your on_register_report handler must return a list of tuples or None; "
+                                 f"it returned '{results}' ({results.__class__.__name__}).")
+                    results = None
+                else:
+                    for i, r in enumerate(results):
+                        if r is None:
+                            continue
+                        if not isinstance(r, tuple):
+                            if mode == 'compact':
+                                logger.error("Your on_register_report handler must return a tuple or None; "
+                                             f"it returned '{r}' ({r.__class__.__name__}).")
+                            elif mode == 'full':
+                                logger.error("Your on_register_report handler must return a list of tuples or None; "
+                                             f"The first item from the list was '{r}' ({r.__class__.__name__}).")
+                            results[i] = None
+                    # If we used compact mode, prepend the r_id to each result
+                    # (this is already there when using the full mode)
+                    if mode == 'compact':
+                        results = [(report['report_descriptions'][i]['r_id'], *results[i])
+                                   for i in range(len(report['report_descriptions'])) if isinstance(results[i], tuple)]
+            report_requests.append(results)
+        utils.validate_report_request_tuples(report_requests, mode=mode)
 
         for i, report_request in enumerate(report_requests):
             if report_request is None or len(report_request) == 0 or all(rrq is None for rrq in report_request):

+ 6 - 6
openleadr/utils.py

@@ -653,7 +653,7 @@ def get_next_event_from_deque(deque):
     return event
 
 
-def validate_report_request_tuples(list_of_report_requests, full_mode=False):
+def validate_report_request_tuples(list_of_report_requests, mode='full'):
     if len(list_of_report_requests) == 0:
         return
     for report_requests in list_of_report_requests:
@@ -666,7 +666,7 @@ def validate_report_request_tuples(list_of_report_requests, full_mode=False):
             # Check if it is a tuple
             elif not isinstance(rrq, tuple):
                 report_requests[i] = None
-                if full_mode:
+                if mode == 'full':
                     logger.error("Your on_register_report handler did not return a list of tuples. "
                                  f"The first item from the list was '{rrq}' ({rrq.__class__.__name__}).")
                 else:
@@ -676,7 +676,7 @@ def validate_report_request_tuples(list_of_report_requests, full_mode=False):
             # Check if it has the correct length
             elif not len(rrq) in (3, 4):
                 report_requests[i] = None
-                if full_mode:
+                if mode == 'full':
                     logger.error("Your on_register_report handler returned tuples of the wrong length. "
                                  f"It should be 3 or 4. It returned: '{rrq}'.")
                 else:
@@ -686,7 +686,7 @@ def validate_report_request_tuples(list_of_report_requests, full_mode=False):
             # Check if the first element is callable
             elif not callable(rrq[1]):
                 report_requests[i] = None
-                if full_mode:
+                if mode == 'full':
                     logger.error(f"Your on_register_report handler did not return the correct tuple. "
                                  "It should return a list of (r_id, callback, sampling_interval) or "
                                  "(r_id, callback, sampling_interval, reporting_interval) tuples, where "
@@ -704,7 +704,7 @@ def validate_report_request_tuples(list_of_report_requests, full_mode=False):
             # Check if the second element is a timedelta
             elif not isinstance(rrq[2], timedelta):
                 report_requests[i] = None
-                if full_mode:
+                if mode == 'full':
                     logger.error(f"Your on_register_report handler did not return the correct tuple. "
                                  "It should return a list of (r_id, callback, sampling_interval) or "
                                  "(r_id, callback, sampling_interval, reporting_interval) tuples, where "
@@ -720,7 +720,7 @@ def validate_report_request_tuples(list_of_report_requests, full_mode=False):
             # Check if the third element is a timedelta (if it exists)
             elif len(rrq) == 4 and not isinstance(rrq[3], timedelta):
                 report_requests[i] = None
-                if full_mode:
+                if mode == 'full':
                     logger.error(f"Your on_register_report handler did not return the correct tuple. "
                                  "It should return a list of (r_id, callback, sampling_interval) or "
                                  "(r_id, callback, sampling_interval, reporting_interval) tuples, where "

+ 25 - 5
test/test_reports.py

@@ -26,7 +26,7 @@ async def lookup_ven(ven_name=None, ven_id=None):
     """
     Look up a ven by its name or ID
     """
-    return {'ven_id': '1234'}
+    return {'ven_id': 'ven1234'}
 
 async def receive_data(data, future=None):
     if future:
@@ -582,7 +582,7 @@ async def test_different_on_register_report_handlers(caplog):
     server.add_handler('on_register_report', on_register_report_returning_string)
     await client.register_reports(client.reports)
     assert len(client.report_requests) == 0
-    assert "Your on_register_report handler must return a tuple; it returned 'Hello There' (str)." in caplog.messages
+    assert "Your on_register_report handler must return a tuple or None; it returned 'Hello There' (str)." in caplog.messages
     caplog.clear()
 
     server.add_handler('on_register_report', on_register_report_returning_uncallable_first_element)
@@ -627,13 +627,13 @@ async def test_different_on_register_report_handlers(caplog):
     server.add_handler('on_register_report', on_register_report_full_returning_string)
     await client.register_reports(client.reports)
     assert len(client.report_requests) == 0
-    assert "Your on_register_report handler must return a list of tuples. It returned 'Hello There' (str)." in caplog.messages
+    assert "Your on_register_report handler must return a list of tuples or None; it returned 'Hello There' (str)." in caplog.messages
     caplog.clear()
 
     server.add_handler('on_register_report', on_register_report_full_returning_list_of_strings)
     await client.register_reports(client.reports)
     assert len(client.report_requests) == 0
-    assert ("Your on_register_report handler did not return a list of tuples. "
+    assert ("Your on_register_report handler must return a list of tuples or None; "
             f"The first item from the list was 'Hello' (str).") in caplog.messages
     caplog.clear()
 
@@ -661,6 +661,7 @@ async def test_different_on_register_report_handlers(caplog):
     assert len(client.report_requests) == 0
     assert ("Your on_register_report handler returned tuples of the wrong length. "
             f"It should be 3 or 4. It returned: '({report_callback}, 'Hello There')'.") in caplog.messages
+
     await server.stop()
     await client.stop()
 
@@ -785,6 +786,25 @@ async def test_report_registration_broken_handlers_raw_message(caplog):
                                   data=msg.encode('utf-8')) as resp:
             assert resp.status == 200
 
-    assert f"Your on_register_report handler must return a list of tuples. It returned 'Hello There Again' (str)." in caplog.messages
+    assert f"Your on_register_report handler must return a list of tuples or None; it returned 'Hello There Again' (str)." in caplog.messages
+
+    await server.stop()
+
 
+@pytest.mark.asyncio
+async def test_register_historic_report():
+    client = OpenADRClient(ven_name='myven',
+                           vtn_url='http://localhost:8080/OpenADR2/Simple/2.0b')
+    client.add_report(report_name='HISTORY_USAGE',
+                      callback=get_historic_data,
+                      measurement='voltage',
+                      resource_id='Device001',
+                      sampling_rate=timedelta(seconds=1))
+    server = OpenADRServer(vtn_id='myvtn')
+    server.add_handler('on_create_party_registration', on_create_party_registration)
+    # server.add_handler('on_register_report', on_register_report_historic)
+    await server.run()
+    await client.run()
+    assert len(server.registered_reports) == 1
+    await client.stop()
     await server.stop()