Bladeren bron

Fixed and streamlined measurements in messages

This formalizes the namespaces that are used for the measurements in Report Descriptions and Event Signals. It fixes some problems with esoteric measurements, and simplifies the XML templating code. It also introduces various additional checks on the measurements to prevent invalid combinations.

Signed-off-by: Stan Janssen <stan.janssen@elaad.nl>
Stan Janssen 4 jaren geleden
bovenliggende
commit
f0f06b5079

+ 166 - 144
openleadr/enums.py

@@ -18,7 +18,7 @@
 A collection of useful enumerations that you can use to construct or
 interpret OpenADR messages. Can also be useful during testing.
 """
-from openleadr import objects
+from openleadr.objects import Measurement, PowerAttributes
 
 
 class Enum(type):
@@ -195,29 +195,28 @@ _CURRENCIES = ("AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "B
                "XFU", "XOF", "XPD", "XPF", "XPF", "XPF", "XPT", "XTS", "XXX", "YER",
                "ZAR", "ZMK", "ZWL")
 
-
 _ACCEPTABLE_UNITS = {'currency': _CURRENCIES,
                      'currencyPerKW': _CURRENCIES,
                      'currencyPerKWh': _CURRENCIES,
-                     'currencyPerTherm': _CURRENCIES,
+                     'currencyPerThm': _CURRENCIES,
                      'current': ('A',),
                      'energyApparent': ('VAh',),
-                     'energyReactive': ('VArh',),
+                     'energyReactive': ('VARh',),
                      'energyReal': ('Wh',),
                      'frequency': ('Hz',),
                      'powerApparent': ('VA',),
-                     'powerReactive': ('VAr',),
+                     'powerReactive': ('VAR',),
                      'powerReal': ('W',),
                      'pulseCount': ('count',),
                      'temperature': ('celsius', 'fahrenheit'),
-                     'therm': ('thm',),
+                     'Therm': ('thm',),
                      'voltage': ('V',)}
 
-
-_MEASUREMENT_DESCRIPTIONS = {'currency': 'Currency',
-                             'currencyPerKW': 'CurrencyPerKW',
-                             'currencyPerKWh': 'CurrencyPerKWh',
-                             'currencyPerTherm': 'CurrencyPerTherm',
+_MEASUREMENT_DESCRIPTIONS = {'currency': 'currency',
+                             'currencyPerKW': 'currencyPerKW',
+                             'currencyPerKWh': 'currencyPerKWh',
+                             'currencyPerThm': 'currency',
+                             'current': 'Current',
                              'energyApparent': 'ApparentEnergy',
                              'energyReactive': 'ReactiveEnergy',
                              'energyReal': 'RealEnergy',
@@ -227,144 +226,167 @@ _MEASUREMENT_DESCRIPTIONS = {'currency': 'Currency',
                              'powerReal': 'RealPower',
                              'pulseCount': 'pulse count',
                              'temperature': 'temperature',
-                             'therm': 'Therm',
+                             'Therm': 'Therm',
                              'voltage': 'Voltage'}
 
+_MEASUREMENT_NAMESPACES = {'currency': 'oadr',
+                           'currencyPerWK': 'oadr',
+                           'currencyPerKWh': 'oadr',
+                           'currencyPerThm': 'oadr',
+                           'current': 'oadr',
+                           'energyApparent': 'power',
+                           'energyReactive': 'power',
+                           'energyReal': 'power',
+                           'frequency': 'oadr',
+                           'powerApparent': 'power',
+                           'powerReactive': 'power',
+                           'powerReal': 'power',
+                           'pulseCount': 'oadr',
+                           'temperature': 'oadr',
+                           'Therm': 'oadr',
+                           'voltage': 'power',
+                           'customUnit': 'oadr'}
+
 
 class MEASUREMENTS(metaclass=Enum):
-    VOLTAGE = objects.Measurement(name='voltage',
-                                  description=_MEASUREMENT_DESCRIPTIONS['voltage'],
-                                  unit=_ACCEPTABLE_UNITS['voltage'][0],
-                                  acceptable_units=_ACCEPTABLE_UNITS['voltage'],
-                                  scale='none')
-    ENERGY_REAL = objects.Measurement(name='energyReal',
-                                      description=_MEASUREMENT_DESCRIPTIONS['energyReal'],
-                                      unit=_ACCEPTABLE_UNITS['energyReal'][0],
-                                      acceptable_units=_ACCEPTABLE_UNITS['energyReal'],
-                                      scale='none')
-    REAL_ENERGY = objects.Measurement(name='energyReal',
-                                      description=_MEASUREMENT_DESCRIPTIONS['energyReal'],
-                                      unit=_ACCEPTABLE_UNITS['energyReal'][0],
-                                      acceptable_units=_ACCEPTABLE_UNITS['energyReal'],
-                                      scale='none')
-    ACTIVE_ENERGY = objects.Measurement(name='energyReal',
-                                        description=_MEASUREMENT_DESCRIPTIONS['energyReal'],
-                                        unit=_ACCEPTABLE_UNITS['energyReal'][0],
-                                        acceptable_units=_ACCEPTABLE_UNITS['energyReal'],
-                                        scale='none')
-    ENERGY_REACTIVE = objects.Measurement(name='energyReactive',
-                                          description=_MEASUREMENT_DESCRIPTIONS['energyReactive'],
-                                          unit=_ACCEPTABLE_UNITS['energyReactive'][0],
-                                          acceptable_units=_ACCEPTABLE_UNITS['energyReactive'],
-                                          scale='none')
-    REACTIVE_ENERGY = objects.Measurement(name='energyReactive',
-                                          description=_MEASUREMENT_DESCRIPTIONS['energyReactive'],
-                                          unit=_ACCEPTABLE_UNITS['energyReactive'][0],
-                                          acceptable_units=_ACCEPTABLE_UNITS['energyReactive'],
-                                          scale='none')
-    ENERGY_APPARENT = objects.Measurement(name='energyApparent',
-                                          description=_MEASUREMENT_DESCRIPTIONS['energyApparent'],
-                                          unit=_ACCEPTABLE_UNITS['energyApparent'][0],
-                                          acceptable_units=_ACCEPTABLE_UNITS['energyApparent'],
-                                          scale='none')
-    APPARENT_ENERGY = objects.Measurement(name='energyApparent',
-                                          description=_MEASUREMENT_DESCRIPTIONS['energyApparent'],
-                                          unit=_ACCEPTABLE_UNITS['energyApparent'][0],
-                                          acceptable_units=_ACCEPTABLE_UNITS['energyApparent'],
-                                          scale='none')
-    ACTIVE_POWER = objects.Measurement(name='powerReal',
-                                       description=_MEASUREMENT_DESCRIPTIONS['powerReal'],
-                                       unit=_ACCEPTABLE_UNITS['powerReal'][0],
-                                       acceptable_units=_ACCEPTABLE_UNITS['powerReal'],
-                                       scale='none',
-                                       power_attributes=objects.PowerAttributes(hertz=50,
-                                                                                voltage=230,
-                                                                                ac=True))
-    REAL_POWER = objects.Measurement(name='powerReal',
-                                     description=_MEASUREMENT_DESCRIPTIONS['powerReal'],
-                                     unit=_ACCEPTABLE_UNITS['powerReal'][0],
-                                     acceptable_units=_ACCEPTABLE_UNITS['powerReal'],
-                                     scale='none',
-                                     power_attributes=objects.PowerAttributes(hertz=50,
-                                                                              voltage=230,
-                                                                              ac=True))
-    POWER_REAL = objects.Measurement(name='powerReal',
-                                     description=_MEASUREMENT_DESCRIPTIONS['powerReal'],
-                                     unit=_ACCEPTABLE_UNITS['powerReal'][0],
-                                     acceptable_units=_ACCEPTABLE_UNITS['powerReal'],
-                                     scale='none',
-                                     power_attributes=objects.PowerAttributes(hertz=50,
-                                                                              voltage=230,
-                                                                              ac=True))
-    REACTIVE_POWER = objects.Measurement(name='powerReactive',
-                                         description=_MEASUREMENT_DESCRIPTIONS['powerReactive'],
-                                         unit=_ACCEPTABLE_UNITS['powerReactive'][0],
-                                         acceptable_units=_ACCEPTABLE_UNITS['powerReactive'],
-                                         scale='none',
-                                         power_attributes=objects.PowerAttributes(hertz=50,
-                                                                                  voltage=230,
-                                                                                  ac=True))
-    POWER_REACTIVE = objects.Measurement(name='powerReactive',
-                                         description=_MEASUREMENT_DESCRIPTIONS['powerReactive'],
-                                         unit=_ACCEPTABLE_UNITS['powerReactive'][0],
-                                         acceptable_units=_ACCEPTABLE_UNITS['powerReactive'],
-                                         scale='none',
-                                         power_attributes=objects.PowerAttributes(hertz=50,
-                                                                                  voltage=230,
-                                                                                  ac=True))
-    APPARENT_POWER = objects.Measurement(name='powerApparent',
-                                         description=_MEASUREMENT_DESCRIPTIONS['powerApparent'],
-                                         unit=_ACCEPTABLE_UNITS['powerApparent'][0],
-                                         acceptable_units=_ACCEPTABLE_UNITS['powerApparent'],
-                                         scale='none',
-                                         power_attributes=objects.PowerAttributes(hertz=50,
-                                                                                  voltage=230,
-                                                                                  ac=True))
-    POWER_APPARENT = objects.Measurement(name='powerApparent',
-                                         description=_MEASUREMENT_DESCRIPTIONS['powerApparent'],
-                                         unit=_ACCEPTABLE_UNITS['powerApparent'][0],
-                                         acceptable_units=_ACCEPTABLE_UNITS['powerApparent'],
-                                         scale='none',
-                                         power_attributes=objects.PowerAttributes(hertz=50,
-                                                                                  voltage=230,
-                                                                                  ac=True))
-    FREQUENCY = objects.Measurement(name='frequency',
-                                    description=_MEASUREMENT_DESCRIPTIONS['frequency'],
-                                    unit=_ACCEPTABLE_UNITS['frequency'][0],
-                                    acceptable_units=_ACCEPTABLE_UNITS['frequency'],
-                                    scale='none')
-    PULSE_COUNT = objects.Measurement(name='pulseCount',
-                                      description=_MEASUREMENT_DESCRIPTIONS['pulseCount'],
-                                      unit=_ACCEPTABLE_UNITS['pulseCount'][0],
-                                      acceptable_units=_ACCEPTABLE_UNITS['pulseCount'],
-                                      scale='none')
-    TEMPERATURE = objects.Measurement(name='temperature',
-                                      description=_MEASUREMENT_DESCRIPTIONS['temperature'],
-                                      unit=_ACCEPTABLE_UNITS['temperature'][0],
-                                      acceptable_units=_ACCEPTABLE_UNITS['temperature'],
-                                      scale='none')
-    THERM = objects.Measurement(name='therm',
-                                description=_MEASUREMENT_DESCRIPTIONS['therm'],
-                                unit=_ACCEPTABLE_UNITS['therm'][0],
-                                acceptable_units=_ACCEPTABLE_UNITS['therm'],
+    VOLTAGE = Measurement(name='voltage',
+                          description=_MEASUREMENT_DESCRIPTIONS['voltage'],
+                          unit=_ACCEPTABLE_UNITS['voltage'][0],
+                          acceptable_units=_ACCEPTABLE_UNITS['voltage'],
+                          scale='none')
+    CURRENT = Measurement(name='current',
+                          description=_MEASUREMENT_DESCRIPTIONS['current'],
+                          unit=_ACCEPTABLE_UNITS['current'][0],
+                          acceptable_units=_ACCEPTABLE_UNITS['current'],
+                          scale='none')
+    ENERGY_REAL = Measurement(name='energyReal',
+                              description=_MEASUREMENT_DESCRIPTIONS['energyReal'],
+                              unit=_ACCEPTABLE_UNITS['energyReal'][0],
+                              acceptable_units=_ACCEPTABLE_UNITS['energyReal'],
+                              scale='none')
+    REAL_ENERGY = Measurement(name='energyReal',
+                              description=_MEASUREMENT_DESCRIPTIONS['energyReal'],
+                              unit=_ACCEPTABLE_UNITS['energyReal'][0],
+                              acceptable_units=_ACCEPTABLE_UNITS['energyReal'],
+                              scale='none')
+    ACTIVE_ENERGY = Measurement(name='energyReal',
+                                description=_MEASUREMENT_DESCRIPTIONS['energyReal'],
+                                unit=_ACCEPTABLE_UNITS['energyReal'][0],
+                                acceptable_units=_ACCEPTABLE_UNITS['energyReal'],
                                 scale='none')
-    CURRENCY = objects.Measurement(name='currency',
-                                   description=_MEASUREMENT_DESCRIPTIONS['currency'],
+    ENERGY_REACTIVE = Measurement(name='energyReactive',
+                                  description=_MEASUREMENT_DESCRIPTIONS['energyReactive'],
+                                  unit=_ACCEPTABLE_UNITS['energyReactive'][0],
+                                  acceptable_units=_ACCEPTABLE_UNITS['energyReactive'],
+                                  scale='none')
+    REACTIVE_ENERGY = Measurement(name='energyReactive',
+                                  description=_MEASUREMENT_DESCRIPTIONS['energyReactive'],
+                                  unit=_ACCEPTABLE_UNITS['energyReactive'][0],
+                                  acceptable_units=_ACCEPTABLE_UNITS['energyReactive'],
+                                  scale='none')
+    ENERGY_APPARENT = Measurement(name='energyApparent',
+                                  description=_MEASUREMENT_DESCRIPTIONS['energyApparent'],
+                                  unit=_ACCEPTABLE_UNITS['energyApparent'][0],
+                                  acceptable_units=_ACCEPTABLE_UNITS['energyApparent'],
+                                  scale='none')
+    APPARENT_ENERGY = Measurement(name='energyApparent',
+                                  description=_MEASUREMENT_DESCRIPTIONS['energyApparent'],
+                                  unit=_ACCEPTABLE_UNITS['energyApparent'][0],
+                                  acceptable_units=_ACCEPTABLE_UNITS['energyApparent'],
+                                  scale='none')
+    ACTIVE_POWER = Measurement(name='powerReal',
+                               description=_MEASUREMENT_DESCRIPTIONS['powerReal'],
+                               unit=_ACCEPTABLE_UNITS['powerReal'][0],
+                               acceptable_units=_ACCEPTABLE_UNITS['powerReal'],
+                               scale='none',
+                               power_attributes=PowerAttributes(hertz=50,
+                                                                voltage=230,
+                                                                ac=True))
+    REAL_POWER = Measurement(name='powerReal',
+                             description=_MEASUREMENT_DESCRIPTIONS['powerReal'],
+                             unit=_ACCEPTABLE_UNITS['powerReal'][0],
+                             acceptable_units=_ACCEPTABLE_UNITS['powerReal'],
+                             scale='none',
+                             power_attributes=PowerAttributes(hertz=50,
+                                                              voltage=230,
+                                                              ac=True))
+    POWER_REAL = Measurement(name='powerReal',
+                             description=_MEASUREMENT_DESCRIPTIONS['powerReal'],
+                             unit=_ACCEPTABLE_UNITS['powerReal'][0],
+                             acceptable_units=_ACCEPTABLE_UNITS['powerReal'],
+                             scale='none',
+                             power_attributes=PowerAttributes(hertz=50,
+                                                              voltage=230,
+                                                              ac=True))
+    REACTIVE_POWER = Measurement(name='powerReactive',
+                                 description=_MEASUREMENT_DESCRIPTIONS['powerReactive'],
+                                 unit=_ACCEPTABLE_UNITS['powerReactive'][0],
+                                 acceptable_units=_ACCEPTABLE_UNITS['powerReactive'],
+                                 scale='none',
+                                 power_attributes=PowerAttributes(hertz=50,
+                                                                  voltage=230,
+                                                                  ac=True))
+    POWER_REACTIVE = Measurement(name='powerReactive',
+                                 description=_MEASUREMENT_DESCRIPTIONS['powerReactive'],
+                                 unit=_ACCEPTABLE_UNITS['powerReactive'][0],
+                                 acceptable_units=_ACCEPTABLE_UNITS['powerReactive'],
+                                 scale='none',
+                                 power_attributes=PowerAttributes(hertz=50,
+                                                                  voltage=230,
+                                                                  ac=True))
+    APPARENT_POWER = Measurement(name='powerApparent',
+                                 description=_MEASUREMENT_DESCRIPTIONS['powerApparent'],
+                                 unit=_ACCEPTABLE_UNITS['powerApparent'][0],
+                                 acceptable_units=_ACCEPTABLE_UNITS['powerApparent'],
+                                 scale='none',
+                                 power_attributes=PowerAttributes(hertz=50,
+                                                                  voltage=230,
+                                                                  ac=True))
+    POWER_APPARENT = Measurement(name='powerApparent',
+                                 description=_MEASUREMENT_DESCRIPTIONS['powerApparent'],
+                                 unit=_ACCEPTABLE_UNITS['powerApparent'][0],
+                                 acceptable_units=_ACCEPTABLE_UNITS['powerApparent'],
+                                 scale='none',
+                                 power_attributes=PowerAttributes(hertz=50,
+                                                                  voltage=230,
+                                                                  ac=True))
+    FREQUENCY = Measurement(name='frequency',
+                            description=_MEASUREMENT_DESCRIPTIONS['frequency'],
+                            unit=_ACCEPTABLE_UNITS['frequency'][0],
+                            acceptable_units=_ACCEPTABLE_UNITS['frequency'],
+                            scale='none')
+    PULSE_COUNT = Measurement(name='pulseCount',
+                              description=_MEASUREMENT_DESCRIPTIONS['pulseCount'],
+                              unit=_ACCEPTABLE_UNITS['pulseCount'][0],
+                              acceptable_units=_ACCEPTABLE_UNITS['pulseCount'],
+                              pulse_factor=1000)
+    TEMPERATURE = Measurement(name='temperature',
+                              description=_MEASUREMENT_DESCRIPTIONS['temperature'],
+                              unit=_ACCEPTABLE_UNITS['temperature'][0],
+                              acceptable_units=_ACCEPTABLE_UNITS['temperature'],
+                              scale='none')
+    THERM = Measurement(name='Therm',
+                        description=_MEASUREMENT_DESCRIPTIONS['Therm'],
+                        unit=_ACCEPTABLE_UNITS['Therm'][0],
+                        acceptable_units=_ACCEPTABLE_UNITS['Therm'],
+                        scale='none')
+    CURRENCY = Measurement(name='currency',
+                           description=_MEASUREMENT_DESCRIPTIONS['currency'],
+                           unit=_CURRENCIES[0],
+                           acceptable_units=_CURRENCIES,
+                           scale='none')
+    CURRENCY_PER_KW = Measurement(name='currencyPerKW',
+                                  description=_MEASUREMENT_DESCRIPTIONS['currencyPerKW'],
+                                  unit=_CURRENCIES[0],
+                                  acceptable_units=_CURRENCIES,
+                                  scale='none')
+    CURRENCY_PER_KWH = Measurement(name='currencyPerKWh',
+                                   description=_MEASUREMENT_DESCRIPTIONS['currencyPerKWh'],
+                                   unit=_CURRENCIES[0],
+                                   acceptable_units=_CURRENCIES,
+                                   scale='none')
+    CURRENCY_PER_THM = Measurement(name='currencyPerThm',
+                                   description=_MEASUREMENT_DESCRIPTIONS['currencyPerThm'],
                                    unit=_CURRENCIES[0],
                                    acceptable_units=_CURRENCIES,
                                    scale='none')
-    CURRENCY_PER_KW = objects.Measurement(name='currencyPerKW',
-                                          description=_MEASUREMENT_DESCRIPTIONS['currencyPerKW'],
-                                          unit=_CURRENCIES[0],
-                                          acceptable_units=_CURRENCIES,
-                                          scale='none')
-    CURRENCY_PER_KWH = objects.Measurement(name='currencyPerKWh',
-                                           description=_MEASUREMENT_DESCRIPTIONS['currencyPerKWh'],
-                                           unit=_CURRENCIES[0],
-                                           acceptable_units=_CURRENCIES,
-                                           scale='none')
-    CURRENCY_PER_THERM = objects.Measurement(name='currencyPerTherm',
-                                             description=_MEASUREMENT_DESCRIPTIONS['currencyPerTherm'],
-                                             unit=_CURRENCIES[0],
-                                             acceptable_units=_CURRENCIES,
-                                             scale='none')

+ 33 - 32
openleadr/objects.py

@@ -18,6 +18,7 @@ from dataclasses import dataclass, field, asdict, is_dataclass
 from typing import List, Dict
 from datetime import datetime, timezone, timedelta
 from openleadr import utils
+from openleadr import enums
 
 
 @dataclass
@@ -129,6 +130,37 @@ class Interval:
     uid: int = None
 
 
+@dataclass
+class SamplingRate:
+    min_period: timedelta = None
+    max_period: timedelta = None
+    on_change: bool = False
+
+
+@dataclass
+class PowerAttributes:
+    hertz: int = 50
+    voltage: int = 230
+    ac: bool = True
+
+
+@dataclass
+class Measurement:
+    name: str
+    description: str
+    unit: str
+    acceptable_units: List[str] = field(repr=False, default_factory=list)
+    scale: str = None
+    power_attributes: PowerAttributes = None
+    pulse_factor: int = None
+    ns: str = 'power'
+
+    def __post_init__(self):
+        if self.name not in enums._MEASUREMENT_NAMESPACES:
+            self.name = 'customUnit'
+        self.ns = enums._MEASUREMENT_NAMESPACES[self.name]
+
+
 @dataclass
 class EventSignal:
     intervals: List[Interval]
@@ -138,6 +170,7 @@ class EventSignal:
     current_value: float = None
     targets: List[Target] = None
     targets_by_type: Dict = None
+    measurement: Measurement = None
 
     def __post_init__(self):
         if self.targets is None and self.targets_by_type is None:
@@ -199,38 +232,6 @@ class Response:
     request_id: str
 
 
-@dataclass
-class SamplingRate:
-    min_period: timedelta = None
-    max_period: timedelta = None
-    on_change: bool = False
-
-
-@dataclass
-class PowerAttributes:
-    hertz: int = 50
-    voltage: int = 230
-    ac: bool = True
-
-
-@dataclass
-class Measurement:
-    name: str
-    description: str
-    unit: str
-    acceptable_units: List[str] = field(repr=False, default_factory=list)
-    scale: str = None
-    power_attributes: PowerAttributes = None
-
-    def __post_init__(self):
-        if self.name not in ('voltage', 'energyReal', 'energyReactive',
-                             'energyApparent', 'powerReal', 'powerApparent',
-                             'powerReactive', 'frequency',  'pulseCount', 'temperature',
-                             'therm', 'currency', 'currencyPerKW', 'currencyPerKWh',
-                             'currencyPerTherm'):
-            self.name = 'customUnit'
-
-
 @dataclass
 class ReportDescription:
     r_id: str                           # Identifies a specific datapoint in a report

+ 21 - 0
openleadr/preflight.py

@@ -54,6 +54,16 @@ def _preflight_oadrRegisterReport(message_payload):
             if 'measurement' in report_description and report_description['measurement'] is not None:
                 utils.validate_report_measurement_dict(report_description['measurement'])
 
+        # Add the correct namespace to the measurement
+        for report_description in report['report_descriptions']:
+            if 'measurement' in report_description and report_description['measurement'] is not None:
+                if report_description['measurement']['name'] in enums._MEASUREMENT_NAMESPACES:
+                    measurement_name = report_description['measurement']['name']
+                    measurement_ns = enums._MEASUREMENT_NAMESPACES[measurement_name]
+                    report_description['measurement']['ns'] = measurement_ns
+                else:
+                    raise ValueError("The Measurement Name is unknown")
+
 
 def _preflight_oadrDistributeEvent(message_payload):
     if 'parse_duration' not in globals():
@@ -98,6 +108,17 @@ def _preflight_oadrDistributeEvent(message_payload):
                                    "This will be corrected.")
                     event_signal['current_value'] = 0
 
+    # Add the correct namespace to the measurement
+    for event in message_payload['events']:
+        for event_signal in event['event_signals']:
+            if 'measurement' in event_signal and event_signal['measurement'] is not None:
+                if event_signal['measurement']['name'] in enums._MEASUREMENT_NAMESPACES:
+                    measurement_name = event_signal['measurement']['name']
+                    measurement_ns = enums._MEASUREMENT_NAMESPACES[measurement_name]
+                    event_signal['measurement']['ns'] = measurement_ns
+                else:
+                    raise ValueError("The Measurement Name is unknown")
+
     # Check that there is a valid oadrResponseRequired value for each Event
     for event in message_payload['events']:
         if 'response_required' not in event:

+ 1 - 3
openleadr/templates/parts/eiEventSignal.xml

@@ -26,9 +26,7 @@
     <ei:signalName>{{ event_signal.signal_name }}</ei:signalName>
     <ei:signalType>{{ event_signal.signal_type }}</ei:signalType>
     <ei:signalID>{{ event_signal.signal_id }}</ei:signalID>
-    {% if event_signal.measurement is defined and event_signal.measurement is not none %}
-        {% include 'parts/eventSignalEmix.xml' %}
-    {% endif %}
+    {% include 'parts/eventSignalEmix.xml' %}
     {% if event_signal.current_value is defined and event_signal.current_value is not none %}
     <ei:currentValue>
         <ei:payloadFloat>

+ 7 - 5
openleadr/templates/parts/eventSignalEmix.xml

@@ -1,8 +1,10 @@
-  {% if event_signal.measurement %}
-  <{% if event_signal.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:{{ event_signal.measurement.name }} xmlns:scale="http://docs.oasis-open.org/ns/emix/2011/06/siscale" xmlns:power="http://docs.oasis-open.org/ns/emix/2011/06/power" >
-    <{% if event_signal.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemDescription>{{ event_signal.measurement.description }}</{% if event_signal.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemDescription>
-    <{% if event_signal.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemUnits>{{ event_signal.measurement.unit }}</{% if event_signal.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemUnits>
+  {% if event_signal.measurement is defined and event_signal.measurement is not none %}
+  <{{ event_signal.measurement.ns }}:{{ event_signal.measurement.name }} xmlns:scale="http://docs.oasis-open.org/ns/emix/2011/06/siscale" xmlns:power="http://docs.oasis-open.org/ns/emix/2011/06/power" >
+    <{{ event_signal.measurement.ns }}:itemDescription>{{ event_signal.measurement.description }}</{{ event_signal.measurement.ns }}:itemDescription>
+    <{{ event_signal.measurement.ns }}:itemUnits>{{ event_signal.measurement.unit }}</{{ event_signal.measurement.ns }}:itemUnits>
+    {% if event_signal.measurement.pulse_factor %}<oadr:pulseFactor>{{ event_signal.measurement.pulse_factor }}</oadr:pulseFactor>{% else %}
     <scale:siScaleCode>{{ event_signal.measurement.scale }}</scale:siScaleCode>
+    {% endif %}
     {% if event_signal.measurement.power_attributes %}
     <power:powerAttributes>
       <power:hertz>{{ event_signal.measurement.power_attributes.hertz }}</power:hertz>
@@ -10,5 +12,5 @@
       <power:ac>{{ event_signal.measurement.power_attributes.ac|booleanformat }}</power:ac>
     </power:powerAttributes>
     {% endif %}
-  </{% if event_signal.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:{{ event_signal.measurement.name }}>
+  </{{ event_signal.measurement.ns }}:{{ event_signal.measurement.name }}>
   {% endif %}

+ 6 - 4
openleadr/templates/parts/reportDescriptionEmix.xml

@@ -1,8 +1,10 @@
   {% if report_description.measurement is defined and report_description.measurement is not none %}
-  <{% if report_description.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:{{ report_description.measurement.name }} xmlns:scale="http://docs.oasis-open.org/ns/emix/2011/06/siscale" xmlns:power="http://docs.oasis-open.org/ns/emix/2011/06/power" >
-    <{% if report_description.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemDescription>{{ report_description.measurement.description }}</{% if report_description.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemDescription>
-    <{% if report_description.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemUnits>{{ report_description.measurement.unit }}</{% if report_description.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:itemUnits>
+  <{{ report_description.measurement.ns }}:{{ report_description.measurement.name }} xmlns:scale="http://docs.oasis-open.org/ns/emix/2011/06/siscale" xmlns:power="http://docs.oasis-open.org/ns/emix/2011/06/power" >
+    <{{ report_description.measurement.ns }}:itemDescription>{{ report_description.measurement.description }}</{{ report_description.measurement.ns }}:itemDescription>
+    <{{ report_description.measurement.ns }}:itemUnits>{{ report_description.measurement.unit }}</{{ report_description.measurement.ns }}:itemUnits>
+    {% if report_description.measurement.pulse_factor %}<oadr:pulseFactor>{{ report_description.measurement.pulse_factor }}</oadr:pulseFactor>{% else %}
     <scale:siScaleCode>{{ report_description.measurement.scale }}</scale:siScaleCode>
+    {% endif %}
     {% if report_description.measurement.power_attributes %}
     <power:powerAttributes>
       <power:hertz>{{ report_description.measurement.power_attributes.hertz }}</power:hertz>
@@ -10,5 +12,5 @@
       <power:ac>{{ report_description.measurement.power_attributes.ac|booleanformat }}</power:ac>
     </power:powerAttributes>
     {% endif %}
-  </{% if report_description.measurement.name == 'customUnit' %}oadr{% else %}power{% endif %}:{{ report_description.measurement.name }}>
+  </{{ report_description.measurement.ns }}:{{ report_description.measurement.name }}>
   {% endif %}

+ 12 - 33
openleadr/utils.py

@@ -17,6 +17,7 @@
 from datetime import datetime, timedelta, timezone
 from dataclasses import is_dataclass, asdict
 from collections import OrderedDict
+from openleadr import enums
 import asyncio
 import itertools
 import re
@@ -82,6 +83,9 @@ def normalize_dict(ordered_dict):
             key = key[4:]
         elif key.startswith('ei'):
             key = key[2:]
+        # Don't normalize the measurement descriptions
+        if key in enums._MEASUREMENT_NAMESPACES:
+            return key
         key = re.sub(r'([a-z])([A-Z])', r'\1_\2', key)
         if '-' in key:
             key = key.replace('-', '_')
@@ -183,43 +187,18 @@ def normalize_dict(ordered_dict):
                 descriptions = [descriptions]
             for description in descriptions:
                 # We want to make the identification of the measurement universal
-                if 'voltage' in description:
-                    name, item = 'voltage', description.pop('voltage')
-                elif 'power_real' in description:
-                    name, item = 'powerReal', description.pop('power_real')
-                elif 'power_apparent' in description:
-                    name, item = 'powerApparent', description.pop('power_apparent')
-                elif 'power_reactive' in description:
-                    name, item = 'powerReactive', description.pop('power_reactive')
-                elif 'energy_real' in description:
-                    name, item = 'energyReal', description.pop('energy_real')
-                elif 'energy_apparent' in description:
-                    name, item = 'energyApparent', description.pop('energy_apparent')
-                elif 'energy_reactive' in description:
-                    name, item = 'energyReactive', description.pop('energy_reactive')
-                elif 'frequency' in description:
-                    name, item = 'frequency', description.pop('frequency')
-                elif 'pulse_count' in description:
-                    name, item = 'pulseCount', description.pop('pulse_count')
-                elif 'temperature' in description:
-                    name, item = 'temperature', description.pop('temperature')
-                elif 'therm' in description:
-                    name, item = 'therm', description.pop('therm')
-                elif 'currency' in description:
-                    name, item = 'currency', description.pop('currency')
-                elif 'currency_per_kw' in description:
-                    name, item = 'currencyPerKW', description.pop('currency_per_kw')
-                elif 'currency_per_kwh' in description:
-                    name, item = 'currencyPerKWh', description.pop('currency_per_kwh')
-                elif 'currency_per_therm' in description:
-                    name, item = 'currencyPerTherm', description.pop('currency_per_therm')
-                elif 'custom_unit' in description:
-                    name, item = 'customUnit', description.pop('custom_unit')
+                for measurement in enums._MEASUREMENT_NAMESPACES:
+                    if measurement in description:
+                        name, item = measurement, description.pop(measurement)
+                        break
                 else:
                     break
                 item['description'] = item.pop('item_description', None)
                 item['unit'] = item.pop('item_units', None)
-                item['scale'] = item.pop('si_scale_code', None)
+                if 'si_scale_code' in item:
+                    item['scale'] = item.pop('si_scale_code')
+                if 'pulse_factor' in item:
+                    item['pulse_factor'] = item.pop('pulse_factor')
                 description['measurement'] = {'name': name,
                                               **item}
             d[key + 's'] = descriptions

+ 44 - 2
test/test_message_conversion.py

@@ -15,12 +15,13 @@
 # limitations under the License.
 
 from openleadr.utils import generate_id, group_targets_by_type
-from openleadr.messaging import create_message, parse_message
+from openleadr.messaging import create_message, parse_message, validate_xml_schema
 from openleadr import enums
 from pprint import pprint
 from termcolor import colored
 from datetime import datetime, timezone, timedelta
 import pytest
+from dataclasses import asdict
 
 DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 
@@ -69,6 +70,32 @@ def create_dummy_event(ven_id):
              'response_required': 'always'}
     return event
 
+reports = [{'report_id': generate_id(),
+            'duration': timedelta(seconds=3600),
+            'report_descriptions': [{'r_id': generate_id(),
+                                     'report_subject': {'resource_id': 'resource001'},
+                                     'report_data_source': {'resource_id': 'resource001'},
+                                     'report_type': 'usage',
+                                     'measurement': asdict(measurement),
+                                     'reading_type': 'Direct Read',
+                                     'market_context': 'http://MarketContext1',
+                                     'sampling_rate': {'min_period': timedelta(seconds=10), 'max_period': timedelta(seconds=30), 'on_change': False}} for measurement in enums.MEASUREMENTS.values],
+            'report_specifier_id': generate_id(),
+            'report_name': 'METADATA_HISTORY_USAGE',
+            'report_request_id': None,
+            'created_date_time': datetime.now(timezone.utc)}]
+
+for report in reports:
+  for rd in report['report_descriptions']:
+    rd['measurement'].pop('acceptable_units')
+    rd['measurement'].pop('ns')
+    if rd['measurement']['power_attributes'] is None:
+      rd['measurement'].pop('power_attributes')
+    if rd['measurement']['scale'] is None:
+      rd['measurement'].pop('scale')
+    if rd['measurement']['pulse_factor'] is None:
+      rd['measurement'].pop('pulse_factor')
+
 testcases = [
 ('oadrCanceledOpt', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, opt_id=generate_id())),
 ('oadrCanceledPartyRegistration', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, registration_id=generate_id(), ven_id='123ABC')),
@@ -122,6 +149,7 @@ testcases = [
                                                                        'specifier_payloads': [{'r_id': 'd6e2e07485',
                                                                                              'reading_type': 'Direct Read'}]}}])),
 ('oadrDistributeEvent', dict(request_id=generate_id(), response={'request_id': 123, 'response_code': 200, 'response_description': 'OK'}, events=[create_dummy_event(ven_id='123ABC')], vtn_id='VTN123')),
+('oadrDistributeEvent', dict(request_id=generate_id(), response={'request_id': 123, 'response_code': 200, 'response_description': 'OK'}, events=[create_dummy_event(ven_id='123ABC'), create_dummy_event(ven_id='123ABC')], vtn_id='VTN123')),
 ('oadrPoll', dict(ven_id='123ABC')),
 ('oadrQueryRegistration', dict(request_id=generate_id())),
 ('oadrRegisteredReport', dict(ven_id='VEN123', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
@@ -177,6 +205,7 @@ testcases = [
                                                                                            'report_type': 'usage',
                                                                                            'measurement': {'name': 'energyReal',
                                                                                                            'description': 'RealEnergy',
+                                                                                                           'ns': 'power',
                                                                                                            'unit': 'Wh',
                                                                                                            'scale': 'n'},
                                                                                            'reading_type': 'Direct Read',
@@ -187,6 +216,7 @@ testcases = [
                                                                                            'report_type': 'usage',
                                                                                            'measurement': {'name': 'powerReal',
                                                                                                            'description': 'RealPower',
+                                                                                                           'ns': 'power',
                                                                                                            'unit': 'W',
                                                                                                            'scale': 'n',
                                                                                                            'power_attributes': {'hertz': 50, 'voltage': 230, 'ac': True}},
@@ -204,6 +234,7 @@ testcases = [
                                                                                            'report_type': 'usage',
                                                                                            'measurement': {'name': 'energyReal',
                                                                                                            'description': 'RealEnergy',
+                                                                                                           'ns': 'power',
                                                                                                            'unit': 'Wh',
                                                                                                            'scale': 'n'},
                                                                                            'reading_type': 'Direct Read',
@@ -214,6 +245,7 @@ testcases = [
                                                                                            'report_type': 'usage',
                                                                                            'measurement': {'name': 'powerReal',
                                                                                                            'description': 'RealPower',
+                                                                                                           'ns': 'power',
                                                                                                            'unit': 'W',
                                                                                                            'scale': 'n',
                                                                                                            'power_attributes': {'hertz': 50, 'voltage': 230, 'ac': True}},
@@ -224,6 +256,7 @@ testcases = [
                                                                   'report_specifier_id': '789ed6cd4e_history_usage',
                                                                   'report_name': 'METADATA_HISTORY_USAGE',
                                                                   'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)}], 'ven_id': 's3cc244ee6'}),
+('oadrRegisterReport', {'ven_id': 'ven123', 'request_id': generate_id(), 'reports': reports}),
 ('oadrResponse', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, ven_id='123ABC')),
 ('oadrResponse', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': None}, ven_id='123ABC')),
 ('oadrUpdatedReport', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, ven_id='123ABC', cancel_report={'request_id': generate_id(), 'report_request_id': [generate_id(), generate_id(), generate_id()], 'report_to_follow': False, 'ven_id': '123ABC'})),
@@ -248,6 +281,15 @@ testcases = [
 @pytest.mark.parametrize('message_type,data', testcases)
 def test_message(message_type, data):
     message = create_message(message_type, **data)
-    print(message)
     parsed = parse_message(message)[1]
+    if message_type == 'oadrRegisterReport':
+        for report in data['reports']:
+            for rd in report['report_descriptions']:
+                if 'measurement' in rd:
+                    rd['measurement'].pop('ns')
+    if message_type == 'oadrDistributeEvent':
+        for event in data['events']:
+            for signal in event['event_signals']:
+                if 'measurement' in signal:
+                    signal['measurement'].pop('ns')
     assert parsed == data