Browse Source

Add initial Message Signatures infrastructure

Stan Janssen 3 years ago
parent
commit
10b967fef8
52 changed files with 1006 additions and 541 deletions
  1. 1 0
      MANIFEST.in
  2. 3 3
      pyopenadr/client.py
  3. 48 0
      pyopenadr/messaging.py
  4. 4 10
      pyopenadr/service/vtn_service.py
  5. 150 0
      pyopenadr/signature.py
  6. 7 10
      pyopenadr/templates/oadrCancelOpt.xml
  7. 7 10
      pyopenadr/templates/oadrCancelPartyRegistration.xml
  8. 8 11
      pyopenadr/templates/oadrCancelReport.xml
  9. 10 13
      pyopenadr/templates/oadrCanceledOpt.xml
  10. 11 14
      pyopenadr/templates/oadrCanceledPartyRegistration.xml
  11. 17 20
      pyopenadr/templates/oadrCanceledReport.xml
  12. 23 26
      pyopenadr/templates/oadrCreateOpt.xml
  13. 15 18
      pyopenadr/templates/oadrCreatePartyRegistration.xml
  14. 9 12
      pyopenadr/templates/oadrCreateReport.xml
  15. 31 34
      pyopenadr/templates/oadrCreatedEvent.xml
  16. 39 42
      pyopenadr/templates/oadrCreatedPartyRegistration.xml
  17. 17 20
      pyopenadr/templates/oadrCreatedReport.xml
  18. 14 17
      pyopenadr/templates/oadrDistributeEvent.xml
  19. 5 0
      pyopenadr/templates/oadrPayload.xml
  20. 5 8
      pyopenadr/templates/oadrPoll.xml
  21. 5 8
      pyopenadr/templates/oadrQueryRegistration.xml
  22. 30 33
      pyopenadr/templates/oadrRegisterReport.xml
  23. 13 16
      pyopenadr/templates/oadrRegisteredReport.xml
  24. 8 11
      pyopenadr/templates/oadrRequestEvent.xml
  25. 5 8
      pyopenadr/templates/oadrRequestReregistration.xml
  26. 14 17
      pyopenadr/templates/oadrResponse.xml
  27. 24 27
      pyopenadr/templates/oadrUpdateReport.xml
  28. 26 29
      pyopenadr/templates/oadrUpdatedReport.xml
  29. 2 2
      pyopenadr/templates/parts/eiActivePeriod.xml
  30. 1 1
      pyopenadr/templates/parts/eiEvent.xml
  31. 1 1
      pyopenadr/templates/parts/eiEventSignal.xml
  32. 1 1
      pyopenadr/templates/parts/oadrReportRequest.xml
  33. 1 2
      pyopenadr/templates/parts/reportDescriptionEmix.xml
  34. 8 0
      pyopenadr/templates/signatures/KeyInfo.xml
  35. 6 0
      pyopenadr/templates/signatures/Signature.xml
  36. 8 0
      pyopenadr/templates/signatures/SignedInfo.xml
  37. 10 0
      pyopenadr/templates/signatures/prop.xml
  38. 72 94
      pyopenadr/utils.py
  39. 1 1
      setup.py
  40. 2 1
      test/conformance/test_conformance_001.py
  41. 4 1
      test/conformance/test_conformance_002.py
  42. 23 1
      test/conformance/test_conformance_006.py
  43. 4 3
      test/conformance/test_conformance_008.py
  44. 2 2
      test/conformance/test_conformance_009.py
  45. 2 1
      test/conformance/test_conformance_014.py
  46. 57 0
      test/conformance/test_conformance_021.py
  47. 0 0
      test/fixtures/__init__.py
  48. 100 0
      test/fixtures/simple_server.py
  49. 60 0
      test/integration_tests/test_client_registration.py
  50. 2 1
      test/test_message_conversion.py
  51. 27 12
      test/test_schema.py
  52. 63 0
      test/test_signatures.py

+ 1 - 0
MANIFEST.in

@@ -1,2 +1,3 @@
 include pyopenadr/templates/*.xml
 include pyopenadr/templates/parts/*.xml
+include pyopenadr/templates/signatures/*.xml

+ 3 - 3
pyopenadr/client.py

@@ -5,8 +5,8 @@ OpenADR Client for Python
 import xmltodict
 import random
 import aiohttp
-from jinja2 import Environment, PackageLoader, select_autoescape
-from pyopenadr.utils import parse_message, create_message, new_request_id, peek, generate_id
+from pyopenadr.utils import new_request_id, peek, generate_id
+from pyopenadr.messaging import create_message, parse_message
 from pyopenadr import enums
 from datetime import datetime, timedelta, timezone
 from http import HTTPStatus
@@ -287,7 +287,7 @@ class OpenADRClient:
 
     async def _perform_request(self, service, message):
         if self.debug:
-            print(f"Sending {message}")
+            print(f"Client is sending {message}")
         url = f"{self.vtn_url}/{service}"
         async with self.client_session.post(url, data=message) as req:
             if req.status != HTTPStatus.OK:

+ 48 - 0
pyopenadr/messaging.py

@@ -0,0 +1,48 @@
+import xmltodict
+from jinja2 import Environment, PackageLoader, select_autoescape
+
+from .utils import *
+from .signature import *
+from .preflight import preflight_message
+
+def parse_message(data):
+    """
+    Parse a message and distill its usable parts. Returns a message type and payload.
+    """
+    message_dict = xmltodict.parse(data, process_namespaces=True, namespaces=NAMESPACES)
+    message_type, message_payload = message_dict['oadrPayload']['oadrSignedObject'].popitem()
+    return message_type, normalize_dict(message_payload)
+
+def create_message(message_type, **message_payload):
+    """
+    This creates an OpenADR message. This consists
+    """
+    preflight_message(message_type, message_payload)
+    signed_object = indent_xml(TEMPLATES.get_template(f'{message_type}.xml').render(**message_payload))
+    signature = create_signature(signed_object)
+
+    envelope = TEMPLATES.get_template('oadrPayload.xml')
+    msg = envelope.render(template=f'{message_type}',
+                          signature=signature,
+                          signed_object=signed_object)
+    return msg
+
+# Settings for jinja2
+TEMPLATES = Environment(loader=PackageLoader('pyopenadr', 'templates'))
+TEMPLATES.filters['datetimeformat'] = datetimeformat
+TEMPLATES.filters['timedeltaformat'] = timedeltaformat
+TEMPLATES.filters['booleanformat'] = booleanformat
+
+# Settings for xmltodict
+NAMESPACES = {
+    'http://docs.oasis-open.org/ns/energyinterop/201110': None,
+    'http://openadr.org/oadr-2.0b/2012/07': None,
+    'urn:ietf:params:xml:ns:icalendar-2.0': None,
+    'http://docs.oasis-open.org/ns/energyinterop/201110/payloads': None,
+    'http://docs.oasis-open.org/ns/emix/2011/06': None,
+    'urn:ietf:params:xml:ns:icalendar-2.0:stream': None,
+    'http://docs.oasis-open.org/ns/emix/2011/06/power': None,
+    'http://docs.oasis-open.org/ns/emix/2011/06/siscale': None,
+    'http://www.w3.org/2000/09/xmldsig#': None,
+    'http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties': None
+}

+ 4 - 10
pyopenadr/service/vtn_service.py

@@ -6,14 +6,9 @@ from aiohttp import web
 from jinja2 import Environment, PackageLoader, select_autoescape
 
 from .. import errors
-from ..utils import parse_message, indent_xml, datetimeformat, timedeltaformat, booleanformat
+from ..messaging import create_message, parse_message
 
 class VTNService:
-    templates = Environment(loader=PackageLoader('pyopenadr', 'templates'),
-                            autoescape=select_autoescape(['html', 'xml']))
-    templates.filters['datetimeformat'] = datetimeformat
-    templates.filters['timedeltaformat'] = timedeltaformat
-    templates.filters['booleanformat'] = booleanformat
 
     def __init__(self, vtn_id):
         self.vtn_id = vtn_id
@@ -36,10 +31,9 @@ class VTNService:
             response_type, response_payload = await handler(message_payload)
             response_payload['vtn_id'] = self.vtn_id
 
-            # Get the relevant template and create the XML response
-            template = self.templates.get_template(f'{response_type}.xml')
-            template.render(**response_payload)
-            response = web.Response(text=indent_xml(template.render(**response_payload)),
+            # Create the XML response
+            msg = create_message(response_type, **response_payload)
+            response = web.Response(text=msg,
                                     status=HTTPStatus.OK,
                                     content_type='application/xml')
 

+ 150 - 0
pyopenadr/signature.py

@@ -0,0 +1,150 @@
+"""
+Utility functions for XML Message Signatures
+"""
+
+import hmac
+import hashlib
+from base64 import b64encode
+import re
+from datetime import datetime, timedelta, timezone
+import xmltodict
+from uuid import uuid4
+from .utils import datetimeformat, timedeltaformat, booleanformat
+from cryptography import x509
+
+from xml.etree.ElementTree import canonicalize
+from io import StringIO
+
+from jinja2 import Environment, PackageLoader, select_autoescape
+
+TEMPLATES = Environment(loader=PackageLoader('pyopenadr', 'templates'))
+
+TEMPLATES.filters['datetimeformat'] = datetimeformat
+TEMPLATES.filters['timedeltaformat'] = timedeltaformat
+TEMPLATES.filters['booleanformat'] = booleanformat
+
+REPLAY_PROTECT_MAX_TIME_DELTA = timedelta(seconds=5)
+
+with open("/home/stan/Ontwikkeling/ElaadNL/pyopenadr/cert.pem", "rb") as file:
+    certificate = x509.load_pem_x509_certificate(file.read())
+    MODULUS = b64encode(certificate.public_key().public_numbers().n.to_bytes(512, 'big')).decode('ascii')
+    EXPONENT = b64encode(certificate.public_key().public_numbers().e.to_bytes(4, 'big')).decode('ascii')
+
+def create_signature(xml_message):
+    """
+    This creates the signature for the given object. It will return the complete
+    Signature section that can be pasted into the XML message.
+    """
+    # Convert it to its canonical C14n form
+    signed_object_canonical = canonicalize(xml_message)
+
+    # Calculate the of this section
+    print("Calculating the SHA256 hash of this object")
+    print(signed_object_canonical)
+    digest_value_signed_object = calculate_digest(signed_object_canonical)
+
+    print()
+    print("The signature value is")
+    print(digest_value_signed_object)
+
+
+    # Generate the prop and calculate the digest
+    prop = render('signatures/prop.xml', timestamp=datetime.now(timezone.utc), nonce=uuid4().hex)
+    digest_value_prop = calculate_digest(prop)
+
+    # Construct the SignedInfo object
+    references = [{'id': 'oadrSignedObject',
+                   'digest_value': digest_value_signed_object},
+                  {'id': 'prop',
+                   'digest_value': digest_value_prop}]
+    signed_info = render('signatures/SignedInfo.xml', references=references)
+
+    # Construct the KeyInfo object
+    key_info = render('signatures/KeyInfo.xml', modulus=MODULUS, exponent=EXPONENT)
+
+    # Calculate the signature for the SignedInfo object
+    signature_value = calculate_digest(signed_info)
+
+    # Render the complete Signature section
+    signature = render('signatures/Signature.xml',
+                       signed_info=signed_info,
+                       signature_value=signature_value,
+                       prop=prop,
+                       key_info=key_info,
+                       canonicalize_output=False)
+    return signature
+
+def validate_message(xml_message):
+    """
+    This validates the message.
+
+    1. Verify the digest of the SignedInfo element against the SignatureValue
+    2. Verify the digest of the oadrSignedObject against the value in the DigestValue field.
+    3. Verify the presence of a ReplayProtect field and that the time is no more different than 5 seconds.
+    """
+
+    # Extract the SignedInfo part
+    signed_info = extract(xml_message, 'SignedInfo')
+    signed_info_canonical = canonicalize(signed_info)
+    signed_info_dict = xmltodict.parse(signed_info_canonical)
+
+    # Verify the digest of the SignedInfo element
+    signed_info_digest = calculate_digest(signed_info)
+
+    # Verify the digest of the oadrSignedObject
+    signed_object = extract(xml_message, 'oadrSignedObject')
+    signed_object_canonical = canonicalize(signed_object)
+    signed_object_id = re.search(r'id="(.*?)"', signed_object_canonical, flags=re.I).group(1)
+
+    # breakpoint()
+    signed_info_reference = re.search(fr'<(.*)?Reference.*? URI="#{signed_object_id}".*?>(.*?)</\1Reference>',
+                                      signed_info,
+                                      flags=re.S).group(2)
+
+    signed_info_digest_method = re.search(r'<(.*)?DigestMethod.* Algorithm="(.*?)"', signed_info_reference).group(2)
+    signed_info_digest_value = re.search(r'<(.*)?DigestValue.*?>(.*?)</\1DigestValue>', signed_info_reference).group(2)
+
+    if signed_info_digest_method != "http://www.w3.org/2001/04/xmlenc#sha256":
+        raise ValueError(f"Wrong digest method used: {signed_info_digest_method}")
+
+    signed_object_digest = calculate_digest(signed_object_canonical)
+    if signed_object_digest != signed_info_digest_value:
+        raise ValueError(f"Digest values do not match for oadrSignedObject identified by #{signed_object_id}\n"
+                         f"Provided Digest: {signed_info_digest_value}\n"
+                         f"Calculated Digest: {signed_object_digest}")
+
+
+def calculate_digest(xml_part):
+    """
+    This calculates the digest for the given XML part
+    and returns its base-64 encoded value
+    """
+    hash = hashlib.sha256()
+    hash.update(xml_part.encode('utf-8'))
+    return b64encode(hash.digest()).decode('ascii')
+
+def calculate_signature(xml_part):
+    """
+    This calculates the signature for the entire SignedInfo block.
+    """
+
+def get_tag_id(xml_message, tag):
+    tag = re.search(fr'<(.*)?{tag}.*?id="(.*?)".*?>',
+                    xml_message,
+                    flags=re.S|re.I).group(0)
+
+def extract(xml, tag):
+    # Extract the first occurence of tag and its contents from the xml message
+    section = re.search(fr'<([^:]*:?){tag}[^>]*>.*</\1{tag}>', xml, flags=re.S)
+    if section:
+        return section.group(0)
+    else:
+        return None
+
+def render(template, canonicalize_output=True, **data):
+    t = TEMPLATES.get_template(template)
+    xml = t.render(**data)
+    if canonicalize_output:
+        return canonicalize(xml)
+    else:
+        return xml

+ 7 - 10
pyopenadr/templates/oadrCancelOpt.xml

@@ -1,10 +1,7 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCancelOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-      <ei:optID>{{ opt_id }}</ei:optID>
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrCancelOpt>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCancelOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+    <ei:optID>{{ opt_id }}</ei:optID>
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrCancelOpt>
+</oadrSignedObject>

+ 7 - 10
pyopenadr/templates/oadrCancelPartyRegistration.xml

@@ -1,10 +1,7 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCancelPartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-      <ei:registrationID>{{ registration_id }}</ei:registrationID>
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrCancelPartyRegistration>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCancelPartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+    <ei:registrationID>{{ registration_id }}</ei:registrationID>
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrCancelPartyRegistration>
+</oadrSignedObject>

+ 8 - 11
pyopenadr/templates/oadrCancelReport.xml

@@ -1,11 +1,8 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCancelReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-      <ei:reportRequestID>{{ report_request_id }}</ei:reportRequestID>
-      <reportToFollow xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ report_to_follow|booleanformat }}</reportToFollow>
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrCancelReport>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCancelReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+    <ei:reportRequestID>{{ report_request_id }}</ei:reportRequestID>
+    <reportToFollow xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ report_to_follow|booleanformat }}</reportToFollow>
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrCancelReport>
+</oadrSignedObject>

+ 10 - 13
pyopenadr/templates/oadrCanceledOpt.xml

@@ -1,13 +1,10 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCanceledOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-      </ei:eiResponse>
-      <ei:optID>{{ opt_id }}</ei:optID>
-    </oadrCanceledOpt>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCanceledOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+    </ei:eiResponse>
+    <ei:optID>{{ opt_id }}</ei:optID>
+  </oadrCanceledOpt>
+</oadrSignedObject>

+ 11 - 14
pyopenadr/templates/oadrCanceledPartyRegistration.xml

@@ -1,14 +1,11 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCanceledPartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-      </ei:eiResponse>
-      <ei:registrationID>{{ registration_id }}</ei:registrationID>
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrCanceledPartyRegistration>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCanceledPartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+    </ei:eiResponse>
+    <ei:registrationID>{{ registration_id }}</ei:registrationID>
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrCanceledPartyRegistration>
+</oadrSignedObject>

+ 17 - 20
pyopenadr/templates/oadrCanceledReport.xml

@@ -1,20 +1,17 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCanceledReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-      </ei:eiResponse>
-      <oadrPendingReports>
-        {% for pending_report in pending_reports %}
-        <ei:reportRequestID>{{ pending_report.request_id }}</ei:reportRequestID>
-        {% endfor %}
-      </oadrPendingReports>
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
-      {% endif %}
-    </oadrCanceledReport>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCanceledReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+    </ei:eiResponse>
+    <oadrPendingReports>
+      {% for pending_report in pending_reports %}
+      <ei:reportRequestID>{{ pending_report.request_id }}</ei:reportRequestID>
+      {% endfor %}
+    </oadrPendingReports>
+    {% if ven_id %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% endif %}
+  </oadrCanceledReport>
+</oadrSignedObject>

+ 23 - 26
pyopenadr/templates/oadrCreateOpt.xml

@@ -1,26 +1,23 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCreateOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110" xmlns:xcal="urn:ietf:params:xml:ns:icalendar-2.0" xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06">
-      <ei:optID>{{ opt_id }}</ei:optID>
-      <ei:optType>{{ opt_type }}</ei:optType>
-      <ei:optReason>{{ opt_reason }}</ei:optReason>
-      {% if market_context %}
-      <emix:marketContext>{{ market_context }}</emix:marketContext>
-      {% endif %}
-      <ei:venID>{{ ven_id }}</ei:venID>
-      {% if vavailability %}
-      <xcal:vavailability></xcal:vavailability>
-      {% endif %}
-      <ei:createdDateTime>{{ created_date_time|datetimeformat }} </ei:createdDateTime>
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-      <ei:qualifiedEventID>
-        <ei:eventID>{{ event_id }}</ei:eventID>
-        <ei:modificationNumber>{{ modification_number }}</ei:modificationNumber>
-      </ei:qualifiedEventID>
-      {% for target in targets %}
-      {% include 'parts/eiTarget.xml' %}
-      {% endfor %}
-    </oadrCreateOpt>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCreateOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110" xmlns:xcal="urn:ietf:params:xml:ns:icalendar-2.0" xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06">
+    <ei:optID>{{ opt_id }}</ei:optID>
+    <ei:optType>{{ opt_type }}</ei:optType>
+    <ei:optReason>{{ opt_reason }}</ei:optReason>
+    {% if market_context %}
+    <emix:marketContext>{{ market_context }}</emix:marketContext>
+    {% endif %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% if vavailability %}
+    <xcal:vavailability></xcal:vavailability>
+    {% endif %}
+    <ei:createdDateTime>{{ created_date_time|datetimeformat }} </ei:createdDateTime>
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+    <ei:qualifiedEventID>
+      <ei:eventID>{{ event_id }}</ei:eventID>
+      <ei:modificationNumber>{{ modification_number }}</ei:modificationNumber>
+    </ei:qualifiedEventID>
+    {% for target in targets %}
+    {% include 'parts/eiTarget.xml' %}
+    {% endfor %}
+  </oadrCreateOpt>
+</oadrSignedObject>

+ 15 - 18
pyopenadr/templates/oadrCreatePartyRegistration.xml

@@ -1,18 +1,15 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07">
-  <oadrSignedObject>
-    <oadrCreatePartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
-      {% endif %}
-      <oadrProfileName>{{ profile_name }}</oadrProfileName>
-      <oadrTransportName>{{ transport_name }}</oadrTransportName>
-      <oadrTransportAddress>{{ transport_address }}</oadrTransportAddress>
-      <oadrReportOnly>{{ report_only|booleanformat }}</oadrReportOnly>
-      <oadrXmlSignature>{{ xml_signature|booleanformat }}</oadrXmlSignature>
-      <oadrVenName>{{ ven_name }}</oadrVenName>
-      <oadrHttpPullModel>{{ http_pull_model|booleanformat }}</oadrHttpPullModel>
-    </oadrCreatePartyRegistration>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCreatePartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+    {% if ven_id %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% endif %}
+    <oadrProfileName>{{ profile_name }}</oadrProfileName>
+    <oadrTransportName>{{ transport_name }}</oadrTransportName>
+    <oadrTransportAddress>{{ transport_address }}</oadrTransportAddress>
+    <oadrReportOnly>{{ report_only|booleanformat }}</oadrReportOnly>
+    <oadrXmlSignature>{{ xml_signature|booleanformat }}</oadrXmlSignature>
+    <oadrVenName>{{ ven_name }}</oadrVenName>
+    <oadrHttpPullModel>{{ http_pull_model|booleanformat }}</oadrHttpPullModel>
+  </oadrCreatePartyRegistration>
+</oadrSignedObject>

+ 9 - 12
pyopenadr/templates/oadrCreateReport.xml

@@ -1,12 +1,9 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07">
-  <oadrSignedObject>
-    <oadrCreateReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-      {% for report_request in report_requests %}
-      {% include 'parts/oadrReportRequest.xml' %}
-      {% endfor %}
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrCreateReport>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCreateReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+    {% for report_request in report_requests %}
+    {% include 'parts/oadrReportRequest.xml' %}
+    {% endfor %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrCreateReport>
+</oadrSignedObject>

+ 31 - 34
pyopenadr/templates/oadrCreatedEvent.xml

@@ -1,35 +1,32 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCreatedEvent ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <eiCreatedEvent xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">
-        <ei:eiResponse>
-          <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-          <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-          {% if response.request_id %}
-          <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-          {% endif %}
-        </ei:eiResponse>
-        {% if event_responses %}
-        <ei:eventResponses>
-          {% for event_response in event_responses %}
-          <ei:eventResponse>
-            <ei:responseCode>{{ event_response.response_code }}</ei:responseCode>
-            {% if event_response.response_description %}
-            <ei:responseDescription>{{ event_response.response_description }}</ei:responseDescription>
-            {% endif %}
-            <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ event_response.request_id }}</requestID>
-            <ei:qualifiedEventID>
-              <ei:eventID>{{ event_response.event_id }}</ei:eventID>
-              <ei:modificationNumber>{{ event_response.modification_number }}</ei:modificationNumber>
-            </ei:qualifiedEventID>
-            <ei:optType>{{ event_response.opt_type }}</ei:optType>
-          </ei:eventResponse>
-          {% endfor %}
-        </ei:eventResponses>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCreatedEvent ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <eiCreatedEvent xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">
+      <ei:eiResponse>
+        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+        {% if response.request_id %}
+        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
         {% endif %}
-        <ei:venID>{{ ven_id }}</ei:venID>
-      </eiCreatedEvent>
-    </oadrCreatedEvent>
-  </oadrSignedObject>
-</oadrPayload>
+      </ei:eiResponse>
+      {% if event_responses %}
+      <ei:eventResponses>
+        {% for event_response in event_responses %}
+        <ei:eventResponse>
+          <ei:responseCode>{{ event_response.response_code }}</ei:responseCode>
+          {% if event_response.response_description %}
+          <ei:responseDescription>{{ event_response.response_description }}</ei:responseDescription>
+          {% endif %}
+          <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ event_response.request_id }}</requestID>
+          <ei:qualifiedEventID>
+            <ei:eventID>{{ event_response.event_id }}</ei:eventID>
+            <ei:modificationNumber>{{ event_response.modification_number }}</ei:modificationNumber>
+          </ei:qualifiedEventID>
+          <ei:optType>{{ event_response.opt_type }}</ei:optType>
+        </ei:eventResponse>
+        {% endfor %}
+      </ei:eventResponses>
+      {% endif %}
+      <ei:venID>{{ ven_id }}</ei:venID>
+    </eiCreatedEvent>
+  </oadrCreatedEvent>
+</oadrSignedObject>

+ 39 - 42
pyopenadr/templates/oadrCreatedPartyRegistration.xml

@@ -1,44 +1,41 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCreatedPartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-      </ei:eiResponse>
-      {% if registration_id %}
-      <ei:registrationID>{{ registration_id }}</ei:registrationID>
-      {% else %}
-      <ei:registrationID />
-      {% endif %}
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCreatedPartyRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+    </ei:eiResponse>
+    {% if registration_id %}
+    <ei:registrationID>{{ registration_id }}</ei:registrationID>
+    {% else %}
+    <ei:registrationID />
+    {% endif %}
 
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
-      {% else %}
-      <ei:venID />
-      {% endif %}
+    {% if ven_id %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% else %}
+    <ei:venID />
+    {% endif %}
 
-      <ei:vtnID>{{ vtn_id }}</ei:vtnID>
-      <oadrProfiles>
-      {% for profile in profiles %}
-        <oadrProfile>
-          <oadrProfileName>{{ profile.profile_name }}</oadrProfileName>
-          <oadrTransports>
-            {% for transport in profile.transports %}
-            <oadrTransport>
-              <oadrTransportName>simpleHttp</oadrTransportName>
-            </oadrTransport>
-            {% endfor %}
-          </oadrTransports>
-        </oadrProfile>
-      {% endfor %}
-      </oadrProfiles>
-      {% if requested_oadr_poll_freq %}
-      <oadrRequestedOadrPollFreq>
-        <duration xmlns="urn:ietf:params:xml:ns:icalendar-2.0">{{ requested_oadr_poll_freq|timedeltaformat }}</duration>
-      </oadrRequestedOadrPollFreq>
-      {% endif %}
-    </oadrCreatedPartyRegistration>
-  </oadrSignedObject>
-</oadrPayload>
+    <ei:vtnID>{{ vtn_id }}</ei:vtnID>
+    <oadrProfiles>
+    {% for profile in profiles %}
+      <oadrProfile>
+        <oadrProfileName>{{ profile.profile_name }}</oadrProfileName>
+        <oadrTransports>
+          {% for transport in profile.transports %}
+          <oadrTransport>
+            <oadrTransportName>simpleHttp</oadrTransportName>
+          </oadrTransport>
+          {% endfor %}
+        </oadrTransports>
+      </oadrProfile>
+    {% endfor %}
+    </oadrProfiles>
+    {% if requested_oadr_poll_freq %}
+    <oadrRequestedOadrPollFreq>
+      <duration xmlns="urn:ietf:params:xml:ns:icalendar-2.0">{{ requested_oadr_poll_freq|timedeltaformat }}</duration>
+    </oadrRequestedOadrPollFreq>
+    {% endif %}
+  </oadrCreatedPartyRegistration>
+</oadrSignedObject>

+ 17 - 20
pyopenadr/templates/oadrCreatedReport.xml

@@ -1,20 +1,17 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrCreatedReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-      </ei:eiResponse>
-      <oadrPendingReports>
-        {% for pending_report in pending_reports %}
-        <ei:reportRequestID>{{ pending_report.request_id }}</ei:reportRequestID>
-        {% endfor %}
-      </oadrPendingReports>
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
-      {% endif %}
-    </oadrCreatedReport>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrCreatedReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+    </ei:eiResponse>
+    <oadrPendingReports>
+      {% for pending_report in pending_reports %}
+      <ei:reportRequestID>{{ pending_report.request_id }}</ei:reportRequestID>
+      {% endfor %}
+    </oadrPendingReports>
+    {% if ven_id %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% endif %}
+  </oadrCreatedReport>
+</oadrSignedObject>

+ 14 - 17
pyopenadr/templates/oadrDistributeEvent.xml

@@ -1,17 +1,14 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-	<oadrSignedObject>
-		<oadrDistributeEvent ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-			<ei:eiResponse>
-				<ei:responseCode>{{ response.response_code }}</ei:responseCode>
-				<ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-				<requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-			</ei:eiResponse>
-			<requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-			<ei:vtnID>{{ vtn_id }}</ei:vtnID>
-            {% for event in events %}
-			    {% include 'parts/eiEvent.xml' %}
-            {% endfor %}
-	</oadrDistributeEvent>
-</oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrDistributeEvent ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+    </ei:eiResponse>
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+    <ei:vtnID>{{ vtn_id }}</ei:vtnID>
+    {% for event in events %}
+        {% include 'parts/eiEvent.xml' %}
+    {% endfor %}
+  </oadrDistributeEvent>
+</oadrSignedObject>

+ 5 - 0
pyopenadr/templates/oadrPayload.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<oadr:oadrPayload xmlns:oadr="http://openadr.org/oadr-2.0b/2012/07">
+{% if signature: %}{{ signature|safe }}{% endif %}
+{{ signed_object|safe }}
+</oadr:oadrPayload>

+ 5 - 8
pyopenadr/templates/oadrPoll.xml

@@ -1,8 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07">
-  <oadrSignedObject>
-    <oadrPoll ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrPoll>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrPoll ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrPoll>
+</oadrSignedObject>

+ 5 - 8
pyopenadr/templates/oadrQueryRegistration.xml

@@ -1,8 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrQueryRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
-    </oadrQueryRegistration>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrQueryRegistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+  </oadrQueryRegistration>
+</oadrSignedObject>

+ 30 - 33
pyopenadr/templates/oadrRegisterReport.xml

@@ -1,36 +1,33 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07">
-  <oadrSignedObject>
-    <oadrRegisterReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrRegisterReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ request_id }}</requestID>
 {% for report in reports %}
-      <oadrReport xmlns:xcal="urn:ietf:params:xml:ns:icalendar-2.0" xmlns:strm="urn:ietf:params:xml:ns:icalendar-2.0:stream">
-        {% if report.dtstart %}
-        <xcal:dtstart>
-          <xcal:date-time>{{ report.duration|timedeltaformat }}</xcal:date-time>
-        </xcal:dtstart>
-        {% endif %}
-        {% if report.duration %}
-        <xcal:duration>
-          <xcal:duration>{{ report.duration|timedeltaformat }}</xcal:duration>
-        </xcal:duration>
-        {% endif %}
-        <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
-    {% for report_description in report.report_descriptions %}
-        {% include 'parts/oadrReportDescription.xml' %}
-    {% endfor %}
-        <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>
-        <ei:reportSpecifierID>{{ report.report_specifier_id }}</ei:reportSpecifierID>
-        <ei:reportName>{{ report.report_name }}</ei:reportName>
-        <ei:createdDateTime>{{ report.created_date_time|datetimeformat }}</ei:createdDateTime>
-      </oadrReport>
-{% endfor %}
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
+    <oadrReport xmlns:xcal="urn:ietf:params:xml:ns:icalendar-2.0" xmlns:strm="urn:ietf:params:xml:ns:icalendar-2.0:stream">
+      {% if report.dtstart %}
+      <xcal:dtstart>
+        <xcal:date-time>{{ report.duration|timedeltaformat }}</xcal:date-time>
+      </xcal:dtstart>
       {% endif %}
-      {% if report_request_id %}
-      <ei:reportRequestID>{{ report_request_id }}</ei:reportRequestID>
+      {% if report.duration %}
+      <xcal:duration>
+        <xcal:duration>{{ report.duration|timedeltaformat }}</xcal:duration>
+      </xcal:duration>
       {% endif %}
-    </oadrRegisterReport>
-  </oadrSignedObject>
-</oadrPayload>
+      <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
+  {% for report_description in report.report_descriptions %}
+      {% include 'parts/oadrReportDescription.xml' %}
+  {% endfor %}
+      <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>
+      <ei:reportSpecifierID>{{ report.report_specifier_id }}</ei:reportSpecifierID>
+      <ei:reportName>{{ report.report_name }}</ei:reportName>
+      <ei:createdDateTime>{{ report.created_date_time|datetimeformat }}</ei:createdDateTime>
+    </oadrReport>
+{% endfor %}
+    {% if ven_id %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% endif %}
+    {% if report_request_id %}
+    <ei:reportRequestID>{{ report_request_id }}</ei:reportRequestID>
+    {% endif %}
+  </oadrRegisterReport>
+</oadrSignedObject>

+ 13 - 16
pyopenadr/templates/oadrRegisteredReport.xml

@@ -1,16 +1,13 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07">
-  <oadrSignedObject>
-    <oadrRegisteredReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-      </ei:eiResponse>
-      {% for report_request in report_requests %}
-      {% include 'parts/oadrReportRequest.xml' %}
-      {% endfor %}
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrRegisteredReport>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrRegisteredReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+    </ei:eiResponse>
+    {% for report_request in report_requests %}
+    {% include 'parts/oadrReportRequest.xml' %}
+    {% endfor %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrRegisteredReport>
+</oadrSignedObject>

+ 8 - 11
pyopenadr/templates/oadrRequestEvent.xml

@@ -1,11 +1,8 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07">
-  <oadrSignedObject>
-    <oadrRequestEvent ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <eiRequestEvent xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">
-        <requestID>{{ request_id }}</requestID>
-        <ei:venID>{{ ven_id }}</ei:venID>
-      </eiRequestEvent>
-    </oadrRequestEvent>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrRequestEvent ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <eiRequestEvent xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">
+      <requestID>{{ request_id }}</requestID>
+      <ei:venID>{{ ven_id }}</ei:venID>
+    </eiRequestEvent>
+  </oadrRequestEvent>
+</oadrSignedObject>

+ 5 - 8
pyopenadr/templates/oadrRequestReregistration.xml

@@ -1,8 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrRequestReregistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrRequestReregistration>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrRequestReregistration ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrRequestReregistration>
+</oadrSignedObject>

+ 14 - 17
pyopenadr/templates/oadrResponse.xml

@@ -1,17 +1,14 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrResponse ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        {% if response.request_id %}
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
-        {% else %}
-        <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads" />
-        {% endif %}
-      </ei:eiResponse>
-      <ei:venID>{{ ven_id }}</ei:venID>
-    </oadrResponse>
-  </oadrSignedObject>
-</oadrPayload>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrResponse ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      {% if response.request_id %}
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
+      {% else %}
+      <requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads" />
+      {% endif %}
+    </ei:eiResponse>
+    <ei:venID>{{ ven_id }}</ei:venID>
+  </oadrResponse>
+</oadrSignedObject>

+ 24 - 27
pyopenadr/templates/oadrUpdateReport.xml

@@ -1,29 +1,26 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xmlns:pyld="http://docs.oasis-open.org/ns/energyinterop/201110/payloads" xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrUpdateReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <pyld:requestID>{{ request_id }}</pyld:requestID>
-      {% if reports %}
-      {% for report in reports %}
-      <oadrReport>
-        <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
-        {% if report.report_descriptions %}
-        {% for report_description in report.report_descriptions %}
-        {% include 'parts/oadrReportDescription.xml' %}
-        <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>
-        <ei:reportSpecifierID>{{ report.report_specifier_id }}</ei:reportSpecifierID>
-        {% if report.report_name %}
-        <ei:reportName>{{ report.report_name }}</ei:reportName>
-        {% endif %}
-        <ei:createdDateTime>{{ report.created_date_time|datetimeformat }}</ei:createdDateTime>
-        {% endfor %}
-        {% endif %}
-      </oadrReport>
-      {% endfor %}
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" xmlns:pyld="http://docs.oasis-open.org/ns/energyinterop/201110/payloads" xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06" id="oadrSignedObject">
+  <oadrUpdateReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
+    <pyld:requestID>{{ request_id }}</pyld:requestID>
+    {% if reports %}
+    {% for report in reports %}
+    <oadrReport>
+      <ei:eiReportID>{{ report.report_id }}</ei:eiReportID>
+      {% if report.report_descriptions %}
+      {% for report_description in report.report_descriptions %}
+      {% include 'parts/oadrReportDescription.xml' %}
+      <ei:reportRequestID>{{ report.report_request_id }}</ei:reportRequestID>
+      <ei:reportSpecifierID>{{ report.report_specifier_id }}</ei:reportSpecifierID>
+      {% if report.report_name %}
+      <ei:reportName>{{ report.report_name }}</ei:reportName>
       {% endif %}
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
+      <ei:createdDateTime>{{ report.created_date_time|datetimeformat }}</ei:createdDateTime>
+      {% endfor %}
       {% endif %}
-    </oadrUpdateReport>
-  </oadrSignedObject>
-</oadrPayload>
+    </oadrReport>
+    {% endfor %}
+    {% endif %}
+    {% if ven_id %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% endif %}
+  </oadrUpdateReport>
+</oadrSignedObject>

+ 26 - 29
pyopenadr/templates/oadrUpdatedReport.xml

@@ -1,31 +1,28 @@
-<?xml version="1.0" encoding="utf-8"?>
-<oadrPayload xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://openadr.org/oadr-2.0b/2012/07" xmlns:pyld="http://docs.oasis-open.org/ns/energyinterop/201110/payloads" xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd">
-  <oadrSignedObject>
-    <oadrUpdatedReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
-      <ei:eiResponse>
-        <ei:responseCode>{{ response.response_code }}</ei:responseCode>
-        <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
-        {% if response.request_id %}
-        <pyld:requestID>{{ response.request_id }}</pyld:requestID>
-        {% else %}
-        <pyld:requestID />
-        {% endif %}
-      </ei:eiResponse>
-      {% if cancel_report %}
-      <oadrCancelReport>
-        <pyld:requestID>{{ cancel_report.request_id }}</pyld:requestID>
-        {% for report_request_id in cancel_report.report_request_id %}
-        <ei:reportRequestID>{{ report_request_id }}</ei:reportRequestID>
-        {% endfor %}
-        <pyld:reportToFollow>{{ cancel_report.report_to_follow|booleanformat }}</pyld:reportToFollow>
-        {% if cancel_report.ven_id %}
-        <ei:venID>{{ cancel_report.ven_id }}</ei:venID>
-        {% endif %}
-      </oadrCancelReport>
+<oadrSignedObject xmlns="http://openadr.org/oadr-2.0b/2012/07" id="oadrSignedObject">
+  <oadrUpdatedReport ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110" xmlns:pyld="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">
+    <ei:eiResponse>
+      <ei:responseCode>{{ response.response_code }}</ei:responseCode>
+      <ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
+      {% if response.request_id %}
+      <pyld:requestID>{{ response.request_id }}</pyld:requestID>
+      {% else %}
+      <pyld:requestID />
       {% endif %}
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
+    </ei:eiResponse>
+    {% if cancel_report %}
+    <oadrCancelReport>
+      <pyld:requestID>{{ cancel_report.request_id }}</pyld:requestID>
+      {% for report_request_id in cancel_report.report_request_id %}
+      <ei:reportRequestID>{{ report_request_id }}</ei:reportRequestID>
+      {% endfor %}
+      <pyld:reportToFollow>{{ cancel_report.report_to_follow|booleanformat }}</pyld:reportToFollow>
+      {% if cancel_report.ven_id %}
+      <ei:venID>{{ cancel_report.ven_id }}</ei:venID>
       {% endif %}
-    </oadrUpdatedReport>
-  </oadrSignedObject>
-</oadrPayload>
+    </oadrCancelReport>
+    {% endif %}
+    {% if ven_id %}
+    <ei:venID>{{ ven_id }}</ei:venID>
+    {% endif %}
+  </oadrUpdatedReport>
+</oadrSignedObject>

+ 2 - 2
pyopenadr/templates/parts/eiActivePeriod.xml

@@ -29,5 +29,5 @@
         </ei:x-eiRecovery>
         {% endif %}
     </properties>
-    <components xsi:nil="true" xmlns="urn:ietf:params:xml:ns:icalendar-2.0" />
-</ei:eiActivePeriod>
+    <components xmlns="urn:ietf:params:xml:ns:icalendar-2.0" />
+</ei:eiActivePeriod>

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

@@ -19,4 +19,4 @@
         {% endif %}
     </ei:eiEvent>
     <oadrResponseRequired>{{ event.response_required }}</oadrResponseRequired>
-</oadrEvent>
+</oadrEvent>

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

@@ -26,4 +26,4 @@
         </ei:payloadFloat>
     </ei:currentValue>
     {% endif %}
-</ei:eiEventSignal>
+</ei:eiEventSignal>

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

@@ -47,4 +47,4 @@
       <ei:readingType>{{ report_request.report_specifier.specifier_payload.reading_type }}</ei:readingType>
     </ei:specifierPayload>
   </ei:reportSpecifier>
-</oadrReportRequest>
+</oadrReportRequest>

+ 1 - 2
pyopenadr/templates/parts/reportDescriptionEmix.xml

@@ -78,5 +78,4 @@
     <itemUnits>{{ report_description.voltage.item_units }}</itemUnits>
     <scale:siScaleCode>{{ report_description.voltage.si_scale_code }}</scale:siScaleCode>
   </voltage>
-  {% endif %}
-
+  {% endif %}

+ 8 - 0
pyopenadr/templates/signatures/KeyInfo.xml

@@ -0,0 +1,8 @@
+    <KeyInfo>
+        <KeyValue>
+            <RSAKeyValue>
+                <Modulus>{{ modulus }}</Modulus>
+                <Exponent>{{ exponent }}</Exponent>
+            </RSAKeyValue>
+        </KeyValue>
+    </KeyInfo>

+ 6 - 0
pyopenadr/templates/signatures/Signature.xml

@@ -0,0 +1,6 @@
+<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
+  {{ signed_info|safe }}
+  <SignatureValue>{{ signature_value }}</SignatureValue>
+  {{ key_info|safe }}
+  {{ prop|safe }}
+</Signature>

+ 8 - 0
pyopenadr/templates/signatures/SignedInfo.xml

@@ -0,0 +1,8 @@
+  <SignedInfo>
+    <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
+    <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />{% for reference in references %}
+    <Reference URI="#{{ reference.id }}">
+      <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+      <DigestValue>{{ reference.digest_value }}</DigestValue>
+    </Reference>{% endfor %}
+  </SignedInfo>

+ 10 - 0
pyopenadr/templates/signatures/prop.xml

@@ -0,0 +1,10 @@
+    <Object Id="prop">
+      <SignatureProperties>
+        <SignatureProperty>
+          <dsp:ReplayProtect xmlns:dsp="http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties">
+            <dsp:timestamp>{{ timestamp|datetimeformat }}</dsp:timestamp>
+            <dsp:nonce>{{ nonce }}</dsp:nonce>
+          </dsp:ReplayProtect>
+        </SignatureProperty>
+      </SignatureProperties>
+    </Object>

+ 72 - 94
pyopenadr/utils.py

@@ -1,6 +1,4 @@
 from asyncio import iscoroutine
-import xmltodict
-from jinja2 import Environment, PackageLoader, select_autoescape
 from datetime import datetime, timedelta, timezone
 import random
 import string
@@ -8,11 +6,17 @@ from collections import OrderedDict
 import itertools
 import re
 
-from .preflight import preflight_message
+from pyopenadr import config
 
 DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 DATETIME_FORMAT_NO_MICROSECONDS = "%Y-%m-%dT%H:%M:%SZ"
 
+def new_request_id(*args, **kwargs):
+    return random.choice(string.ascii_lowercase) + ''.join(random.choice(string.hexdigits) for _ in range(9)).lower()
+
+def generate_id(*args, **kwargs):
+    return new_request_id()
+
 def indent_xml(message):
     """
     Indents the XML in a nice way.
@@ -44,61 +48,6 @@ def normalize_dict(ordered_dict):
             key = key.replace('-', '_')
         return key.lower()
 
-    def parse_datetime(value):
-        """
-        Parse an ISO8601 datetime
-        """
-        matches = re.match(r'(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.?(\d{1,6})?\d*Z', value)
-        if matches:
-            year, month, day, hour, minute, second, microsecond = (int(value) for value in matches.groups())
-            return datetime(year, month, day, hour, minute, second, microsecond=microsecond, tzinfo=timezone.utc)
-        else:
-            print(f"{value} did not match format")
-            return value
-
-    def parse_duration(value):
-        """
-        Parse a RFC5545 duration.
-        """
-        # TODO: implement the full regex: matches = re.match(r'(\+|\-)?P((\d+Y)?(\d+M)?(\d+D)?T?(\d+H)?(\d+M)?(\d+S)?)|(\d+W)', value)
-        matches = re.match(r'P(\d+(?:D|W))?T(\d+H)?(\d+M)?(\d+S)?', value)
-        if not matches:
-            return False
-        days = hours = minutes = seconds = 0
-        _days, _hours, _minutes, _seconds = matches.groups()
-        if _days:
-            if _days.endswith("D"):
-                days = int(_days[:-1])
-            elif _days.endswith("W"):
-                days = int(_days[:-1]) * 7
-        if _hours:
-            hours = int(_hours[:-1])
-        if _minutes:
-            minutes = int(_minutes[:-1])
-        if _seconds:
-            seconds = int(_seconds[:-1])
-        return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
-
-    def parse_int(value):
-        matches = re.match(r'^[\d-]+$', value)
-        if not matches:
-            return False
-        else:
-            return int(value)
-
-    def parse_float(value):
-        matches = re.match(r'^[\d.-]+$', value)
-        if not matches:
-            return False
-        else:
-            return float(value)
-
-    def parse_boolean(value):
-        if value == 'true':
-            return True
-        else:
-            return False
-
     d = {}
     for key, value in ordered_dict.items():
         # Interpret values from the dict
@@ -116,8 +65,8 @@ def normalize_dict(ordered_dict):
                     d[key].append(normalize_dict(dict_item))
                 else:
                     d[key].append(item)
-        elif key in ("duration", "startafter", "max_period", "min_period") and isinstance(value, str):
-            d[key] = parse_duration(value) or value
+        elif key in ("duration", "startafter", "max_period", "min_period"):
+            d[key] = parse_duration(value)
         elif "date_time" in key and isinstance(value, str):
             d[key] = parse_datetime(value)
         elif value in ('true', 'false'):
@@ -145,6 +94,13 @@ def normalize_dict(ordered_dict):
             d[key + "s"] = new_targets
             key = key + "s"
 
+        # Dig up the properties inside some specific target identifiers
+        # if key in ("aggregated_pnode", "pnode", "service_delivery_point"):
+        #     d[key] = d[key]["node"]
+
+        # if key in ("end_device_asset", "meter_asset"):
+        #     d[key] = d[key]["mrid"]
+
         # Group all reports as a list of dicts under the key "pending_reports"
         if key == "pending_reports":
             if isinstance(d[key], dict) and 'report_request_id' in d[key] and isinstance(d[key]['report_request_id'], list):
@@ -242,8 +198,8 @@ def normalize_dict(ordered_dict):
         elif isinstance(d[key], dict) and "date_time" in d[key] and len(d[key]) == 1:
             d[key] = d[key]["date_time"]
 
-        # Promote 'properties' item
-        elif isinstance(d[key], dict) and "properties" in d[key] and len(d[key]) == 1:
+        # Promote 'properties' item, discard the unused? 'components' item
+        elif isinstance(d[key], dict) and "properties" in d[key] and len(d[key]) <= 2:
             d[key] = d[key]["properties"]
 
         # Remove all empty dicts
@@ -251,24 +207,62 @@ def normalize_dict(ordered_dict):
             d.pop(key)
     return d
 
-def parse_message(data):
+def parse_datetime(value):
     """
-    Parse a message and distill its usable parts. Returns a message type and payload.
+    Parse an ISO8601 datetime
     """
-    message_dict = xmltodict.parse(data, process_namespaces=True, namespaces=NAMESPACES)
-    message_type, message_payload = message_dict['oadrPayload']['oadrSignedObject'].popitem()
-    return message_type, normalize_dict(message_payload)
+    matches = re.match(r'(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.?(\d{1,6})?\d*Z', value)
+    if matches:
+        year, month, day, hour, minute, second, microsecond = (int(value) for value in matches.groups())
+        return datetime(year, month, day, hour, minute, second, microsecond=microsecond, tzinfo=timezone.utc)
+    else:
+        print(f"{value} did not match format")
+        return value
 
-def create_message(message_type, **message_payload):
-    preflight_message(message_type, message_payload)
-    template = TEMPLATES.get_template(f'{message_type}.xml')
-    return indent_xml(template.render(**message_payload))
+def parse_duration(value):
+    """
+    Parse a RFC5545 duration.
+    """
+    # TODO: implement the full regex: matches = re.match(r'(\+|\-)?P((\d+Y)?(\d+M)?(\d+D)?T?(\d+H)?(\d+M)?(\d+S)?)|(\d+W)', value)
+    if isinstance(value, timedelta):
+        return value
+    matches = re.match(r'P(\d+(?:D|W))?T(\d+H)?(\d+M)?(\d+S)?', value)
+    if not matches:
+        return False
+    days = hours = minutes = seconds = 0
+    _days, _hours, _minutes, _seconds = matches.groups()
+    if _days:
+        if _days.endswith("D"):
+            days = int(_days[:-1])
+        elif _days.endswith("W"):
+            days = int(_days[:-1]) * 7
+    if _hours:
+        hours = int(_hours[:-1])
+    if _minutes:
+        minutes = int(_minutes[:-1])
+    if _seconds:
+        seconds = int(_seconds[:-1])
+    return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
+
+def parse_int(value):
+    matches = re.match(r'^[\d-]+$', value)
+    if not matches:
+        return False
+    else:
+        return int(value)
 
-def new_request_id(*args, **kwargs):
-    return random.choice(string.ascii_lowercase) + ''.join(random.choice(string.hexdigits) for _ in range(9)).lower()
+def parse_float(value):
+    matches = re.match(r'^[\d.-]+$', value)
+    if not matches:
+        return False
+    else:
+        return float(value)
 
-def generate_id(*args, **kwargs):
-    return new_request_id()
+def parse_boolean(value):
+    if value == 'true':
+        return True
+    else:
+        return False
 
 def peek(iterable):
     """
@@ -317,23 +311,7 @@ def booleanformat(value):
             return "true"
         elif value == False:
             return "false"
-    else:
+    elif value in ("true", "false"):
         return value
-
-
-TEMPLATES = Environment(loader=PackageLoader('pyopenadr', 'templates'))
-
-NAMESPACES = {
-    'http://docs.oasis-open.org/ns/energyinterop/201110': None,
-    'http://openadr.org/oadr-2.0b/2012/07': None,
-    'urn:ietf:params:xml:ns:icalendar-2.0': None,
-    'http://docs.oasis-open.org/ns/energyinterop/201110/payloads': None,
-    'http://docs.oasis-open.org/ns/emix/2011/06': None,
-    'urn:ietf:params:xml:ns:icalendar-2.0:stream': None,
-    'http://docs.oasis-open.org/ns/emix/2011/06/power': None,
-    'http://docs.oasis-open.org/ns/emix/2011/06/siscale': None
-}
-
-TEMPLATES.filters['datetimeformat'] = datetimeformat
-TEMPLATES.filters['timedeltaformat'] = timedeltaformat
-TEMPLATES.filters['booleanformat'] = booleanformat
+    else:
+        raise ValueError("A boolean value must be provided.")

+ 1 - 1
setup.py

@@ -15,4 +15,4 @@ setup(name="pyopenadr",
       url="https://git.finetuned.nl/stan/pyopenadr",
       packages=['pyopenadr', 'pyopenadr.service'],
       include_package_data=True,
-      install_requires=['xmltodict', 'aiohttp', 'apscheduler', 'jinja2'])
+      install_requires=['xmltodict', 'aiohttp', 'apscheduler', 'jinja2', 'cryptography'])

+ 2 - 1
test/conformance/test_conformance_001.py

@@ -1,7 +1,8 @@
 import pytest
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
-from pyopenadr.utils import generate_id, create_message, parse_message
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
 from datetime import datetime, timezone, timedelta
 
 

+ 4 - 1
test/conformance/test_conformance_002.py

@@ -1,8 +1,10 @@
 import pytest
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
-from pyopenadr.utils import generate_id, create_message, parse_message
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
 from datetime import datetime, timezone, timedelta
+from pyopenadr.signature import validate_message
 
 from pprint import pprint
 
@@ -50,6 +52,7 @@ async def test_conformance_002():
                          request_id=generate_id(),
                          vtn_id=generate_id(),
                          events=[event])
+    validate_message(msg)
 
     # Parse the message
     parsed_type, parsed_msg = parse_message(msg)

+ 23 - 1
test/conformance/test_conformance_006.py

@@ -1,7 +1,8 @@
 import pytest
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
-from pyopenadr.utils import generate_id, create_message, parse_message
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
 from datetime import datetime, timezone, timedelta
 
 from pprint import pprint
@@ -13,6 +14,24 @@ async def test_conformance_006():
     The presence of any string except “false” in the oadrDistributeEvent
     testEvent element MUST be treated as a trigger for a test event.
     """
+
+    # Monkey patch our own formatter to prevent an error being raised
+    from pyopenadr.messaging import TEMPLATES
+    def booleanformat_monkey(value):
+        """
+        Format a boolean value
+        """
+        if isinstance(value, bool):
+            if value == True:
+                return "true"
+            elif value == False:
+                return "false"
+        else:
+            return value
+
+    booleanformat_original = TEMPLATES.filters['booleanformat']
+    TEMPLATES.filters['booleanformat'] = booleanformat_monkey
+
     event_id = generate_id()
     event = {'event_descriptor':
                 {'event_id': event_id,
@@ -52,3 +71,6 @@ async def test_conformance_006():
     parsed_type, parsed_message = parse_message(msg)
     assert parsed_type == 'oadrDistributeEvent'
     assert parsed_message['events'][0]['event_descriptor']['test_event'] == True
+
+    # Restore the original booleanformat function
+    TEMPLATES.filters['booleanformat'] = booleanformat_original

+ 4 - 3
test/conformance/test_conformance_008.py

@@ -1,7 +1,8 @@
 import pytest
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
-from pyopenadr.utils import generate_id, create_message, parse_message
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
 from datetime import datetime, timezone, timedelta
 
 from pprint import pprint
@@ -23,7 +24,7 @@ async def test_conformance_008_autocorrect():
                  'market_context': 'MarketContext001',
                  'created_date_time': datetime.now(),
                  'event_status': enums.EVENT_STATUS.FAR,
-                 'test_event': "HelloThere",
+                 'test_event': False,
                  'vtn_comment': 'No Comment'},
             'active_period':
                 {'dtstart': datetime.now(),
@@ -72,7 +73,7 @@ async def test_conformance_008_raise():
                  'market_context': 'MarketContext001',
                  'created_date_time': datetime.now(),
                  'event_status': enums.EVENT_STATUS.FAR,
-                 'test_event': "HelloThere",
+                 'test_event': False,
                  'vtn_comment': 'No Comment'},
             'active_period':
                 {'dtstart': datetime.now(),

+ 2 - 2
test/conformance/test_conformance_009.py

@@ -1,13 +1,13 @@
 import pytest
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
-from pyopenadr.utils import generate_id, create_message, parse_message
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
 from datetime import datetime, timezone, timedelta
 
 from pprint import pprint
 import warnings
 
-
 @pytest.mark.asyncio
 async def test_conformance_009_pass():
     """

+ 2 - 1
test/conformance/test_conformance_014.py

@@ -1,7 +1,8 @@
 import pytest
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
-from pyopenadr.utils import generate_id, create_message, parse_message
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
 from datetime import datetime, timezone, timedelta
 
 from pprint import pprint

+ 57 - 0
test/conformance/test_conformance_021.py

@@ -0,0 +1,57 @@
+import pytest
+
+from pyopenadr import OpenADRClient, OpenADRServer, enums
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
+from pyopenadr.objects import Event, EventDescriptor, ActivePeriod, EventSignal, Interval
+from datetime import datetime, timezone, timedelta
+
+from pprint import pprint
+import warnings
+
+from test.fixtures.simple_server import start_server, add_event
+
+@pytest.mark.asyncio
+async def test_conformance_021(start_server):
+    """
+    If venID, vtnID, or eventID value is included in the payload, the receiving
+    entity MUST validate that the ID value is as expected and generate an error
+    if an unexpected value is received.
+    Exception: A VEN MUST NOT generate an error upon receipt of a canceled
+    event whose eventID is not previously known.
+    """
+
+    client = OpenADRClient(ven_name="TestVEN",
+                           vtn_url="http://localhost:8001/OpenADR2/Simple/2.0b")
+    await client.create_party_registration()
+    event = {'event_descriptor':
+                {'event_id': generate_id(),
+                 'modification_number': 0,
+                 'modification_date': datetime.now(),
+                 'priority': 0,
+                 'market_context': 'MarketContext001',
+                 'created_date_time': datetime.now(),
+                 'event_status': enums.EVENT_STATUS.FAR,
+                 'test_event': False,
+                 'vtn_comment': 'No Comment'},
+            'active_period':
+                {'dtstart': datetime.now() + timedelta(minutes=30),
+                 'duration': timedelta(minutes=30)},
+            'event_signals':
+                [{'intervals': [{'duration': timedelta(minutes=10),
+                                 'signal_payload': 1},
+                                {'duration': timedelta(minutes=10),
+                                 'signal_payload': 2},
+                                {'duration': timedelta(minutes=10),
+                                 'signal_payload': 3}],
+                  'signal_name': enums.SIGNAL_NAME.SIMPLE,
+                  'signal_type': enums.SIGNAL_TYPE.DELTA,
+                  'signal_id': generate_id(),
+                  'current_value': 123
+                }]
+        }
+    add_event(ven_id=client.ven_id,
+              event_id = event['event_descriptor']['event_id'],
+              event=event)
+    message_type, message_payload = await client.poll()
+    assert message_type == 'oadrDistributeEvent'

+ 0 - 0
test/fixtures/__init__.py


+ 100 - 0
test/fixtures/simple_server.py

@@ -0,0 +1,100 @@
+from pyopenadr import OpenADRClient, OpenADRServer, enums
+from pyopenadr.utils import generate_id, normalize_dict, timedeltaformat, datetimeformat, booleanformat
+from pyopenadr.messaging import create_message, parse_message
+from datetime import datetime, timezone, timedelta
+
+import asyncio
+import sqlite3
+import pytest
+from aiohttp import web
+import json
+
+SERVER_PORT = 8001
+VEN_NAME = 'myven'
+VTN_ID = "TestVTN"
+
+class EventFormatter(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, timedelta):
+            return timedeltaformat(obj)
+        if isinstance(obj, datetime):
+            return datetimeformat(obj)
+        if isinstance(obj, boolean):
+            return booleanformat(obj)
+        return json.JSONEncoder.default(self, obj)
+
+DB = sqlite3.connect(":memory:")
+with DB:
+    DB.execute("CREATE TABLE vens (ven_id STRING, ven_name STRING, online BOOLEAN, last_seen DATETIME, registration_id STRING)")
+    DB.execute("CREATE TABLE events (event_id STRING, ven_id STRING, request_id STRING, status STRING, event JSON, created_at DATETIME, updated_at DATETIME)")
+
+def lookup_ven(ven_name):
+    with DB:
+        DB.execute("SELECT * FROM vens WHERE ven_name = ?", (ven_name,))
+        ven = cur.fetchone()
+    return ven
+
+def add_ven(ven_name, ven_id, registration_id):
+    with DB:
+        DB.execute("""INSERT INTO vens (ven_id, ven_name, online, last_seen, registration_id)
+                           VALUES (?, ?, ?, ?, ?)""", (ven_id, ven_name, True, datetime.now().replace(microsecond=0), registration_id))
+
+def add_event(ven_id, event_id, event):
+    serialized_event = json.dumps(event, cls=EventFormatter)
+    with DB:
+        DB.execute("""INSERT INTO events (ven_id, event_id, request_id, status, event)
+                           VALUES (?, ?, ?, ?, ?)""", (ven_id, event_id, None, 'new', serialized_event))
+
+async def _on_poll(ven_id, request_id=None):
+    cur = DB.cursor()
+    cur.execute("""SELECT event_id, event FROM events WHERE ven_id = ? AND status = 'new' LIMIT 1""", (ven_id,))
+    result = cur.fetchone()
+    if result:
+        event_id, event = result
+        event_request_id = generate_id()
+        with DB:
+            DB.execute("""UPDATE events SET request_id = ? WHERE event_id = ?""", (event_request_id, event_id))
+        response_type = 'oadrDistributeEvent'
+        response_payload = {'response': {'request_id': request_id,
+                                         'response_code': 200,
+                                         'response_description': 'OK'},
+                            'request_id': event_request_id,
+                            'vtn_id': VTN_ID,
+                            'events': [json.loads(event)]}
+    else:
+        response_type = 'oadrResponse'
+        response_payload = {'response': {'request_id': request_id,
+                                         'response_code': 200,
+                                         'response_description': 'OK'},
+                            'ven_id': ven_id}
+    return response_type, response_payload
+
+async def _on_create_party_registration(payload):
+    registration_id = generate_id()
+    ven_id = generate_id()
+    add_ven(payload['ven_name'], ven_id, registration_id)
+    payload = {'response': {'response_code': 200,
+                            'response_description': 'OK',
+                            'request_id': payload['request_id']},
+               'ven_id': ven_id,
+               'registration_id': registration_id,
+               'profiles': [{'profile_name': '2.0b',
+                             'transports': {'transport_name': 'simpleHttp'}}],
+               'requested_oadr_poll_freq': timedelta(seconds=10)}
+    return 'oadrCreatedPartyRegistration', payload
+
+
+server = OpenADRServer(vtn_id=VTN_ID)
+server.add_handler('on_create_party_registration', _on_create_party_registration)
+server.add_handler('on_poll', _on_poll)
+
+@pytest.fixture
+async def start_server():
+    runner = web.AppRunner(server.app)
+    await runner.setup()
+    site = web.TCPSite(runner, 'localhost', SERVER_PORT)
+    await site.start()
+    print("SERVER IS NOW RUNNING")
+    yield
+    print("SERVER IS NOW STOPPING")
+    await runner.cleanup()

+ 60 - 0
test/integration_tests/test_client_registration.py

@@ -0,0 +1,60 @@
+from pyopenadr import OpenADRClient, OpenADRServer, enums
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
+from datetime import datetime, timezone, timedelta
+
+import asyncio
+import sqlite3
+import pytest
+from aiohttp import web
+
+SERVER_PORT = 8001
+VEN_NAME = 'myven'
+VEN_ID = '1234abcd'
+VTN_ID = "TestVTN"
+
+
+async def _on_create_party_registration(payload):
+    registration_id = generate_id()
+    payload = {'response': {'response_code': 200,
+                            'response_description': 'OK',
+                            'request_id': payload['request_id']},
+               'ven_id': VEN_ID,
+               'registration_id': registration_id,
+               'profiles': [{'profile_name': '2.0b',
+                             'transports': {'transport_name': 'simpleHttp'}}],
+               'requested_oadr_poll_freq': timedelta(seconds=10)}
+    return 'oadrCreatedPartyRegistration', payload
+
+@pytest.fixture
+async def start_server():
+    server = OpenADRServer(vtn_id=VTN_ID)
+    server.add_handler('on_create_party_registration', _on_create_party_registration)
+
+    runner = web.AppRunner(server.app)
+    await runner.setup()
+    site = web.TCPSite(runner, 'localhost', SERVER_PORT)
+    await site.start()
+    yield
+    await runner.cleanup()
+
+
+@pytest.mark.asyncio
+async def test_query_party_registration(start_server):
+    client = OpenADRClient(ven_name=VEN_NAME,
+                           vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b")
+
+    response_type, response_payload = await client.query_registration()
+    assert response_type == 'oadrCreatedPartyRegistration'
+    assert response_payload['vtn_id'] == VTN_ID
+
+@pytest.mark.asyncio
+async def test_create_party_registration(start_server):
+    client = OpenADRClient(ven_name=VEN_NAME,
+                           vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b")
+
+    response_type, response_payload = await client.create_party_registration()
+    assert response_type == 'oadrCreatedPartyRegistration'
+    assert response_payload['ven_id'] == VEN_ID
+
+

+ 2 - 1
test/test_message_conversion.py

@@ -1,4 +1,5 @@
-from pyopenadr.utils import create_message, parse_message, generate_id
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message
 from pyopenadr import enums
 from pprint import pprint
 from termcolor import colored

+ 27 - 12
test/test_schema.py

@@ -1,10 +1,12 @@
-from pyopenadr.utils import create_message, generate_id
+from pyopenadr.utils import generate_id, indent_xml
+from pyopenadr.messaging import create_message
 from pyopenadr import enums
 from lxml import etree
 import os
 from datetime import datetime, timedelta, timezone
 from termcolor import colored
 import jinja2
+from pprint import pprint
 
 DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 SCHEMA_LOCATION = os.path.join('schema', 'oadr_20b.xsd')
@@ -20,7 +22,7 @@ def create_dummy_event(ven_id):
     now = datetime.now(timezone.utc)
     event_id = generate_id()
     active_period = {"dtstart": now + timedelta(minutes=1),
-                     "duration": timedelta(minutes=10)}
+                     "duration": timedelta(minutes=9)}
 
     event_descriptor = {"event_id": event_id,
                         "modification_number": 1,
@@ -56,17 +58,30 @@ def create_dummy_event(ven_id):
              'response_required': 'always'}
     return event
 
-# Test oadrPoll
-def test_message(type, **payload):
+def test_message(msg_type, **payload):
     try:
-        message = create_message(type, **payload)
+        with open(os.path.join('example_messages', f'{msg_type}.xml'), 'w') as file:
+            message = create_message(msg_type, **payload)
+            print(indent_xml(message),file=file)
+            # print(type, file=file)
+            # print("="*len(type), file=file)
+            # print("", file=file)
+            # print("OpenADR payload:", file=file)
+            # print("..code-block:xml", file=file)
+            # print("", file=file)
+            # print(indent_xml(message), file=file)
+            # print("", file=file)
+            # print("pyOpenADR representation:", file=file)
+            # print("..code-block:python3", file=file)
+            # print("", file=file)
+            # pprint(payload, stream=file)
         etree.fromstring(message.encode('utf-8'), parser)
-        print(colored(f"pass: {type} OK", "green"))
+        print(colored(f"pass: {msg_type} OK", "green"))
     except etree.XMLSyntaxError as err:
-        print(colored(f"fail: {type} failed validation: {err}", "red"))
+        print(colored(f"fail: {msg_type} failed validation: {err}", "red"))
         print(message)
     except jinja2.exceptions.UndefinedError as err:
-        print(colored(f"fail: {type} failed message construction: {err}", "yellow"))
+        print(colored(f"fail: {msg_type} failed message construction: {err}", "yellow"))
 
 
 test_message('oadrCanceledOpt', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, opt_id=generate_id())
@@ -188,10 +203,10 @@ test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id'
 #     for reading_type in enums.READING_TYPE.values:
 #         for report_type in enums.REPORT_TYPE.values:
 #             test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id': generate_id(),
-#                                                                                   'report_name': report_name,
-#                                                                                   'created_date_time': datetime.now(timezone.utc),
-#                                                                                   'report_request_id': generate_id(),
-#                                                                                   'report_specifier_id': generate_id(),
+#                                                                                  'report_name': report_name,
+#                                                                                  'created_date_time': datetime.now(timezone.utc),
+#                                                                                  'report_request_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'}],

+ 63 - 0
test/test_signatures.py

@@ -0,0 +1,63 @@
+
+from pyopenadr.utils import generate_id
+from pyopenadr.messaging import create_message, parse_message, validate_message
+from pyopenadr.signature import extract, calculate_digest
+from xml.etree.ElementTree import canonicalize
+from hashlib import sha256
+from base64 import b64encode
+from datetime import datetime, timedelta, timezone
+
+def test_message_validation():
+    msg = create_message('oadrPoll', ven_id='123')
+    parsed_type, parsed_message = parse_message(msg)
+    assert parsed_type == 'oadrPoll'
+    validate_message(msg)
+
+
+
+def test_message_validation_complex():
+    now = datetime.now(timezone.utc)
+    event_id = generate_id()
+    active_period = {"dtstart": now + timedelta(minutes=1),
+                     "duration": timedelta(minutes=9)}
+
+    event_descriptor = {"event_id": event_id,
+                        "modification_number": 1,
+                        "modification_date_time": now,
+                        "priority": 1,
+                        "market_context": "http://MarketContext1",
+                        "created_date_time": now,
+                        "event_status": "near",
+                        "test_event": "false",
+                        "vtn_comment": "This is an event"}
+
+    event_signals = [{"intervals": [{"duration": timedelta(minutes=1), "uid": 1, "signal_payload": 8},
+                                    {"duration": timedelta(minutes=1), "uid": 2, "signal_payload": 10},
+                                    {"duration": timedelta(minutes=1), "uid": 3, "signal_payload": 12},
+                                    {"duration": timedelta(minutes=1), "uid": 4, "signal_payload": 14},
+                                    {"duration": timedelta(minutes=1), "uid": 5, "signal_payload": 16},
+                                    {"duration": timedelta(minutes=1), "uid": 6, "signal_payload": 18},
+                                    {"duration": timedelta(minutes=1), "uid": 7, "signal_payload": 20},
+                                    {"duration": timedelta(minutes=1), "uid": 8, "signal_payload": 10},
+                                    {"duration": timedelta(minutes=1), "uid": 9, "signal_payload": 20}],
+                    "signal_name": "LOAD_CONTROL",
+                    #"signal_name": "simple",
+                    #"signal_type": "level",
+                    "signal_type": "x-loadControlCapacity",
+                    "signal_id": generate_id(),
+                    "current_value": 9.99}]
+
+    event_targets = [{"ven_id": 'VEN001'}, {"ven_id": 'VEN002'}]
+    event = {'active_period': active_period,
+             'event_descriptor': event_descriptor,
+             'event_signals': event_signals,
+             'targets': event_targets,
+             'response_required': 'always'}
+
+    msg = create_message('oadrDistributeEvent',
+                         request_id=generate_id(),
+                         response={'request_id': 123, 'response_code': 200, 'response_description': 'OK'},
+                         events=[event])
+    parsed_type, parsed_msg = parse_message(msg)
+    assert parsed_type == 'oadrDistributeEvent'
+    validate_message(msg)