Browse Source

Add initial Message Signatures infrastructure

Stan Janssen 4 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/*.xml
 include pyopenadr/templates/parts/*.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 xmltodict
 import random
 import random
 import aiohttp
 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 pyopenadr import enums
 from datetime import datetime, timedelta, timezone
 from datetime import datetime, timedelta, timezone
 from http import HTTPStatus
 from http import HTTPStatus
@@ -287,7 +287,7 @@ class OpenADRClient:
 
 
     async def _perform_request(self, service, message):
     async def _perform_request(self, service, message):
         if self.debug:
         if self.debug:
-            print(f"Sending {message}")
+            print(f"Client is sending {message}")
         url = f"{self.vtn_url}/{service}"
         url = f"{self.vtn_url}/{service}"
         async with self.client_session.post(url, data=message) as req:
         async with self.client_session.post(url, data=message) as req:
             if req.status != HTTPStatus.OK:
             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 jinja2 import Environment, PackageLoader, select_autoescape
 
 
 from .. import errors
 from .. import errors
-from ..utils import parse_message, indent_xml, datetimeformat, timedeltaformat, booleanformat
+from ..messaging import create_message, parse_message
 
 
 class VTNService:
 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):
     def __init__(self, vtn_id):
         self.vtn_id = vtn_id
         self.vtn_id = vtn_id
@@ -36,10 +31,9 @@ class VTNService:
             response_type, response_payload = await handler(message_payload)
             response_type, response_payload = await handler(message_payload)
             response_payload['vtn_id'] = self.vtn_id
             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,
                                     status=HTTPStatus.OK,
                                     content_type='application/xml')
                                     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 %}
         {% 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 %}
 {% 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 %}
       {% 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 %}
       {% 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 %}
       {% endif %}
-      {% if ven_id %}
-      <ei:venID>{{ ven_id }}</ei:venID>
+      <ei:createdDateTime>{{ report.created_date_time|datetimeformat }}</ei:createdDateTime>
+      {% endfor %}
       {% endif %}
       {% 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 %}
       {% 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 %}
       {% 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>
         </ei:x-eiRecovery>
         {% endif %}
         {% endif %}
     </properties>
     </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 %}
         {% endif %}
     </ei:eiEvent>
     </ei:eiEvent>
     <oadrResponseRequired>{{ event.response_required }}</oadrResponseRequired>
     <oadrResponseRequired>{{ event.response_required }}</oadrResponseRequired>
-</oadrEvent>
+</oadrEvent>

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

@@ -26,4 +26,4 @@
         </ei:payloadFloat>
         </ei:payloadFloat>
     </ei:currentValue>
     </ei:currentValue>
     {% endif %}
     {% 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:readingType>{{ report_request.report_specifier.specifier_payload.reading_type }}</ei:readingType>
     </ei:specifierPayload>
     </ei:specifierPayload>
   </ei:reportSpecifier>
   </ei:reportSpecifier>
-</oadrReportRequest>
+</oadrReportRequest>

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

@@ -78,5 +78,4 @@
     <itemUnits>{{ report_description.voltage.item_units }}</itemUnits>
     <itemUnits>{{ report_description.voltage.item_units }}</itemUnits>
     <scale:siScaleCode>{{ report_description.voltage.si_scale_code }}</scale:siScaleCode>
     <scale:siScaleCode>{{ report_description.voltage.si_scale_code }}</scale:siScaleCode>
   </voltage>
   </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
 from asyncio import iscoroutine
-import xmltodict
-from jinja2 import Environment, PackageLoader, select_autoescape
 from datetime import datetime, timedelta, timezone
 from datetime import datetime, timedelta, timezone
 import random
 import random
 import string
 import string
@@ -8,11 +6,17 @@ from collections import OrderedDict
 import itertools
 import itertools
 import re
 import re
 
 
-from .preflight import preflight_message
+from pyopenadr import config
 
 
 DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 DATETIME_FORMAT_NO_MICROSECONDS = "%Y-%m-%dT%H:%M:%SZ"
 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):
 def indent_xml(message):
     """
     """
     Indents the XML in a nice way.
     Indents the XML in a nice way.
@@ -44,61 +48,6 @@ def normalize_dict(ordered_dict):
             key = key.replace('-', '_')
             key = key.replace('-', '_')
         return key.lower()
         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 = {}
     d = {}
     for key, value in ordered_dict.items():
     for key, value in ordered_dict.items():
         # Interpret values from the dict
         # Interpret values from the dict
@@ -116,8 +65,8 @@ def normalize_dict(ordered_dict):
                     d[key].append(normalize_dict(dict_item))
                     d[key].append(normalize_dict(dict_item))
                 else:
                 else:
                     d[key].append(item)
                     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):
         elif "date_time" in key and isinstance(value, str):
             d[key] = parse_datetime(value)
             d[key] = parse_datetime(value)
         elif value in ('true', 'false'):
         elif value in ('true', 'false'):
@@ -145,6 +94,13 @@ def normalize_dict(ordered_dict):
             d[key + "s"] = new_targets
             d[key + "s"] = new_targets
             key = key + "s"
             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"
         # Group all reports as a list of dicts under the key "pending_reports"
         if 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):
             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:
         elif isinstance(d[key], dict) and "date_time" in d[key] and len(d[key]) == 1:
             d[key] = d[key]["date_time"]
             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"]
             d[key] = d[key]["properties"]
 
 
         # Remove all empty dicts
         # Remove all empty dicts
@@ -251,24 +207,62 @@ def normalize_dict(ordered_dict):
             d.pop(key)
             d.pop(key)
     return d
     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):
 def peek(iterable):
     """
     """
@@ -317,23 +311,7 @@ def booleanformat(value):
             return "true"
             return "true"
         elif value == False:
         elif value == False:
             return "false"
             return "false"
-    else:
+    elif value in ("true", "false"):
         return value
         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",
       url="https://git.finetuned.nl/stan/pyopenadr",
       packages=['pyopenadr', 'pyopenadr.service'],
       packages=['pyopenadr', 'pyopenadr.service'],
       include_package_data=True,
       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
 import pytest
 
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
 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 datetime import datetime, timezone, timedelta
 
 
 
 

+ 4 - 1
test/conformance/test_conformance_002.py

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

+ 23 - 1
test/conformance/test_conformance_006.py

@@ -1,7 +1,8 @@
 import pytest
 import pytest
 
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
 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 datetime import datetime, timezone, timedelta
 
 
 from pprint import pprint
 from pprint import pprint
@@ -13,6 +14,24 @@ async def test_conformance_006():
     The presence of any string except “false” in the oadrDistributeEvent
     The presence of any string except “false” in the oadrDistributeEvent
     testEvent element MUST be treated as a trigger for a test event.
     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_id = generate_id()
     event = {'event_descriptor':
     event = {'event_descriptor':
                 {'event_id': event_id,
                 {'event_id': event_id,
@@ -52,3 +71,6 @@ async def test_conformance_006():
     parsed_type, parsed_message = parse_message(msg)
     parsed_type, parsed_message = parse_message(msg)
     assert parsed_type == 'oadrDistributeEvent'
     assert parsed_type == 'oadrDistributeEvent'
     assert parsed_message['events'][0]['event_descriptor']['test_event'] == True
     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
 import pytest
 
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
 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 datetime import datetime, timezone, timedelta
 
 
 from pprint import pprint
 from pprint import pprint
@@ -23,7 +24,7 @@ async def test_conformance_008_autocorrect():
                  'market_context': 'MarketContext001',
                  'market_context': 'MarketContext001',
                  'created_date_time': datetime.now(),
                  'created_date_time': datetime.now(),
                  'event_status': enums.EVENT_STATUS.FAR,
                  'event_status': enums.EVENT_STATUS.FAR,
-                 'test_event': "HelloThere",
+                 'test_event': False,
                  'vtn_comment': 'No Comment'},
                  'vtn_comment': 'No Comment'},
             'active_period':
             'active_period':
                 {'dtstart': datetime.now(),
                 {'dtstart': datetime.now(),
@@ -72,7 +73,7 @@ async def test_conformance_008_raise():
                  'market_context': 'MarketContext001',
                  'market_context': 'MarketContext001',
                  'created_date_time': datetime.now(),
                  'created_date_time': datetime.now(),
                  'event_status': enums.EVENT_STATUS.FAR,
                  'event_status': enums.EVENT_STATUS.FAR,
-                 'test_event': "HelloThere",
+                 'test_event': False,
                  'vtn_comment': 'No Comment'},
                  'vtn_comment': 'No Comment'},
             'active_period':
             'active_period':
                 {'dtstart': datetime.now(),
                 {'dtstart': datetime.now(),

+ 2 - 2
test/conformance/test_conformance_009.py

@@ -1,13 +1,13 @@
 import pytest
 import pytest
 
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
 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 datetime import datetime, timezone, timedelta
 
 
 from pprint import pprint
 from pprint import pprint
 import warnings
 import warnings
 
 
-
 @pytest.mark.asyncio
 @pytest.mark.asyncio
 async def test_conformance_009_pass():
 async def test_conformance_009_pass():
     """
     """

+ 2 - 1
test/conformance/test_conformance_014.py

@@ -1,7 +1,8 @@
 import pytest
 import pytest
 
 
 from pyopenadr import OpenADRClient, OpenADRServer, enums
 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 datetime import datetime, timezone, timedelta
 
 
 from pprint import pprint
 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 pyopenadr import enums
 from pprint import pprint
 from pprint import pprint
 from termcolor import colored
 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 pyopenadr import enums
 from lxml import etree
 from lxml import etree
 import os
 import os
 from datetime import datetime, timedelta, timezone
 from datetime import datetime, timedelta, timezone
 from termcolor import colored
 from termcolor import colored
 import jinja2
 import jinja2
+from pprint import pprint
 
 
 DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 SCHEMA_LOCATION = os.path.join('schema', 'oadr_20b.xsd')
 SCHEMA_LOCATION = os.path.join('schema', 'oadr_20b.xsd')
@@ -20,7 +22,7 @@ def create_dummy_event(ven_id):
     now = datetime.now(timezone.utc)
     now = datetime.now(timezone.utc)
     event_id = generate_id()
     event_id = generate_id()
     active_period = {"dtstart": now + timedelta(minutes=1),
     active_period = {"dtstart": now + timedelta(minutes=1),
-                     "duration": timedelta(minutes=10)}
+                     "duration": timedelta(minutes=9)}
 
 
     event_descriptor = {"event_id": event_id,
     event_descriptor = {"event_id": event_id,
                         "modification_number": 1,
                         "modification_number": 1,
@@ -56,17 +58,30 @@ def create_dummy_event(ven_id):
              'response_required': 'always'}
              'response_required': 'always'}
     return event
     return event
 
 
-# Test oadrPoll
-def test_message(type, **payload):
+def test_message(msg_type, **payload):
     try:
     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)
         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:
     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)
         print(message)
     except jinja2.exceptions.UndefinedError as err:
     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())
 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 reading_type in enums.READING_TYPE.values:
 #         for report_type in enums.REPORT_TYPE.values:
 #         for report_type in enums.REPORT_TYPE.values:
 #             test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id': generate_id(),
 #             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_descriptions': [{'r_id': generate_id(),
 #                                                                                                           'report_subjects': [{'ven_id': '123ABC'}, {'ven_id': 'DEF456'}],
 #                                                                                                           'report_subjects': [{'ven_id': '123ABC'}, {'ven_id': 'DEF456'}],
 #                                                                                                           'report_data_sources': [{'ven_id': '123ABC'}],
 #                                                                                                           '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)