Browse Source

Completed Message Signing infrastructure and API

This commit completes the initial version of the Message Signing API.
Stan Janssen 3 years ago
parent
commit
2ee43e02ba

+ 2 - 1
README.md

@@ -11,7 +11,8 @@ You can find documentation here: https://finetuned.nl/pyopenadr
 
 ## Contributing
 
-At this moment, we're finishing off a first usable version. After version 0.3.0, new bug reports and pull requests are most welcome.
+At this moment, we're finishing off a first usable version. After version 0.5.0,
+new bug reports and pull requests are most welcome.
 
 ## Developing
 

+ 1 - 1
VERSION

@@ -1 +1 @@
-0.3.0
+0.4.0

+ 5 - 0
dev_requirements.txt

@@ -0,0 +1,5 @@
+termcolor
+pytest
+pytest-asyncio
+sphinx
+sphinxcontrib-apidoc

+ 12 - 0
docs/_static/css/custom.css

@@ -0,0 +1,12 @@
+body{
+    font-size: 10.5pt;
+}
+
+.sphinxsidebarwrapper{
+    font-size: 9pt;
+    width: 230px;
+}
+
+div.bodywrapper{
+    margin: 0 0 0 260px;
+}

+ 7 - 0
docs/api/modules.rst

@@ -0,0 +1,7 @@
+pyopenadr
+=========
+
+.. toctree::
+   :maxdepth: 4
+
+   pyopenadr

+ 77 - 0
docs/api/pyopenadr.rst

@@ -0,0 +1,77 @@
+pyopenadr package
+=================
+
+Submodules
+----------
+
+pyopenadr.client module
+-----------------------
+
+.. automodule:: pyopenadr.client
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.enums module
+----------------------
+
+.. automodule:: pyopenadr.enums
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.errors module
+-----------------------
+
+.. automodule:: pyopenadr.errors
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.messaging module
+--------------------------
+
+.. automodule:: pyopenadr.messaging
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.objects module
+------------------------
+
+.. automodule:: pyopenadr.objects
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.preflight module
+--------------------------
+
+.. automodule:: pyopenadr.preflight
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.server module
+-----------------------
+
+.. automodule:: pyopenadr.server
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.utils module
+----------------------
+
+.. automodule:: pyopenadr.utils
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: pyopenadr
+   :members:
+   :undoc-members:
+   :show-inheritance:

+ 62 - 0
docs/api/pyopenadr.service.rst

@@ -0,0 +1,62 @@
+pyopenadr.service package
+=========================
+
+Submodules
+----------
+
+pyopenadr.service.event\_service module
+---------------------------------------
+
+.. automodule:: pyopenadr.service.event_service
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.service.opt\_service module
+-------------------------------------
+
+.. automodule:: pyopenadr.service.opt_service
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.service.poll\_service module
+--------------------------------------
+
+.. automodule:: pyopenadr.service.poll_service
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.service.registration\_service module
+----------------------------------------------
+
+.. automodule:: pyopenadr.service.registration_service
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.service.report\_service module
+----------------------------------------
+
+.. automodule:: pyopenadr.service.report_service
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+pyopenadr.service.vtn\_service module
+-------------------------------------
+
+.. automodule:: pyopenadr.service.vtn_service
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: pyopenadr.service
+   :members:
+   :undoc-members:
+   :show-inheritance:

+ 93 - 1
docs/client.rst

@@ -2,14 +2,106 @@
 Client
 ======
 
-The page contains all information about aiohttp Client API.
+An OpenADR Client (Virtual End Node or VEN) usually represents an entity that owns controllable devices. This can be electric vehicles, generators, wind turbines, refrigerated warehouses, et cetera. The client connects to a server, usualy representing a utility company, to discuss possible cooperation on energy usage throughout the day.
+
+In your application, you mostly only have to deal with two things: Events and Reports.
 
 .. _client_events:
 
 Dealing with Events
 ===================
 
+Events are informational or instructional messages from the server (VTN) which inform you of price changes, request load reduction, et cetera. Whenever there is an Event for your VEN, your ``on_event`` handler will be called with the event as its ``payload`` parameter.
+
+The Event consists of three main sections:
+
+1. A time period for when this event is supposed to be active (``active_period``)
+2. A list of Targets to which the Event applies (``target``). This can be the VEN as a whole, or specific groups, assets, geographic areas, et cetera that this VEN represents.
+3. A list of Signals (``signals``), which form the content of the Event. This can be price signals, load reduction signals, et cetera. Each signal has a name, a type, multiple Intervals that contain the relative start times, and some payload value for the client to interpret.
+
+After you evaluate all these properties, you have only one decision to make: Opt In or Opt Out. Your handler must return either the string ``optIn`` or ``optOut``, and pyOpenADR will see to it that your response is correctly formatted for the server.
+
+Example implementation:
+
+.. code-block:: python3
+
+    from openadr import OpenADRClient
+
+    async def on_event(payload):
+        # Check if we can opt in to this event
+        start_time = payload['events'][0]['active_period']['dtstart']
+        duration = payload['events'][0]['active_period']['duration']
+
+        await can_we_do_this(from_time=payload[''])
+        return 'optIn'
+
+
 .. _client_reports:
 
 Dealing with Reports
 ====================
+
+Receiving reports
+-----------------
+
+Upon registration, the server will tell you which reports it has available.
+
+
+Providing reports
+-----------------
+
+If you tell pyOpenADR what reports you are able to provide, and give it a handler that will retrieve those reports from your own systems, pyOpenADR will make sure that the server receives the reports it asks for and at the requested interval.
+
+For example: you can provide 15-minute meter readings for an energy meter at your site. You have a coroutine set up like this:
+
+.. code-block:: python3
+
+    async def get_metervalue():
+        current_value = await meter.read()
+        return current_value
+
+And you configure this report in pyOpenADR using an :ref:`oadrReportDescription` dict:
+
+.. code-block:: python3
+
+    async def main():
+        client = OpenADRClient(ven_name='MyVEN', vtn_url='https://localhost:8080/')
+        report_description = {''}
+        client.add_report({'report'})
+
+The only thing you need to provide is the current value for the item you are reporting. PyOpenADR will format the complete :ref:`oadrReport` message for you.
+
+
+
+.. _client_signing_messages:
+
+Signing outgoing messages
+=========================
+
+You can sign your outgoing messages using a public-private key pair in PEM format. This allows the receiving party to verify that the messages are actually coming from you.
+
+If you want you client to automatically sign your outgoing messages, use the following configuration:
+
+.. code-block:: python3
+
+    async def main():
+        client = OpenADRClient(ven_name='MyVEN', vtn_url='https://localhost:8080/',
+                               cert='/path/to/cert.pem',
+                               key='/path/to/key.pem',
+                               passphrase='my-key-password')
+        ...
+
+.. _client_validating_messages:
+
+Validating incoming messages
+============================
+
+You can validate incoming messages against a public key.
+
+.. code-block:: python3
+
+    async def main():
+        client = OpenADRClient(ven_name='MyVEN', vtn_url='https://localhost:8080/',
+                               verification_cert='/path/to/cert.pem')
+
+This will automatically validate check that incoming messages are signed by the private key that belongs to the provided (public) certificate. If validation fails, you will see a Warning emitted, but the message will not be delivered to your handlers, protecting you from malicious messages being processed by your system. The sending party will see an error message returned.

+ 3 - 1
docs/conf.py

@@ -25,7 +25,7 @@ author = 'Stan Janssen'
 with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'VERSION')) as file:
     release = file.read().strip()
 
-
+print(release)
 
 # -- General configuration ---------------------------------------------------
 
@@ -46,6 +46,7 @@ templates_path = ['_templates']
 # This pattern also affects html_static_path and html_extra_path.
 exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
 
+autoclass_content = 'both'
 
 # -- Options for HTML output -------------------------------------------------
 
@@ -59,5 +60,6 @@ html_theme = 'alabaster'
 # so a file named "default.css" will overwrite the builtin "default.css".
 html_static_path = ['_static']
 
+
 def setup(app):
     app.add_stylesheet('css/custom.css')  # may also be an URL

+ 3 - 3
docs/examples.rst

@@ -1,8 +1,8 @@
 .. _examples:
 
-========
-Examples
-========
+=====================
+Ready-to-Run Examples
+=====================
 
 This page contains examples for pyOpenADR:
 

+ 31 - 0
docs/features.rst

@@ -0,0 +1,31 @@
+.. _feature_tour:
+
+############
+Feature Tour
+############
+
+Automatic registration
+----------------------
+
+
+
+Automatic reporting
+-------------------
+
+
+
+Event-driven
+------------
+
+
+
+Dict-representations
+--------------------
+
+
+
+Message Signing for secure communications
+-----------------------------------------
+
+If you provide a PEM-formatted certificate and key, all outgoing messages will be cryptographically signed. In addition, both the client and server can supply a public PEM-formatted certificate to verify all incoming messages, to make sure they came from a trusted source.
+

+ 19 - 7
docs/index.rst

@@ -7,7 +7,7 @@
 Welcome to pyOpenADR
 ====================
 
-Dead-simple Python implementation of an OpenADR client and server.
+Super-convenient Python implementation of an OpenADR client and server.
 
 Key Features
 ============
@@ -17,6 +17,20 @@ Key Features
 - All messages are represented as simple Python dictionaries. All XML parsing and generation is done for you.
 - You only have to deal with your own logic.
 
+Take a :ref:`feature_tour`!
+
+Project Status
+==============
+
+The current version is |release|.
+
+This project is still a work in progress. Please see our :ref:`roadmap` for information.
+
+License
+=======
+
+This project is licensed under the Apache 2.0 license.
+
 Library Installation
 ====================
 
@@ -67,10 +81,6 @@ This will connect to an OpenADR server (indicated by the vtn_url parameter), han
 
 We have more examples available over at the :ref:`examples` page.
 
-Source Code
-===========
-
-The source code for this project is hosted at GitHub.
 
 Table of Contents
 =================
@@ -79,12 +89,14 @@ Table of Contents
    :name: mastertoc
    :maxdepth: 2
 
+   features
    openadr
    client
    server
-   representations
    examples
-
+   roadmap
+   representations
+   API Reference <api/modules>
 
 Representations of OpenADR payloads
 ===================================

+ 9 - 7
docs/openadr.rst

@@ -2,16 +2,18 @@
 OpenADR Basics
 ==============
 
-If you are coming to this module and are not (yet) familiar with the workings of OpenADR, read this.
+If you are coming to this module and are not (yet) familiar with the OpenADR protocol, read this. Of course, you should also consult the documentation from `the OpenADR website <https://www.openadr.org>`_.
 
 High-level overview
 ===================
 
-OpenADR is a protocol that allows a server (called a Virtual Top Node or VTNs) to communicate 'Events' to connected clients (Called Virtual End Nodes or VENs). These Events are usually energy-related instructions to temporarily increase or reduce the power consumption by one or more devices represented by the VEN. The VEN periodically (typically every 10 seconds or so) sends a Poll request to the VTN to check if there are new events for them.
+OpenADR is a protocol that allows a server (called a Virtual Top Node or VTNs) to communicate 'Events' to connected clients (Called Virtual End Nodes or VENs). These Events are usually energy-related instructions, for instance to temporarily increase or reduce the power consumption by one or more devices represented by the VEN, or te inform the VEN that prices are about to change. The VEN periodically (typically every 10 seconds or so) sends a Poll request to the VTN to check if there are new events for them.
 
-The VEN will then decide whether or not to comply with the request in the Event, and send an Opt In or Opt Out response to the VTN.
+The VEN decides whether or not to comply with the request in the Event, and sends an Opt In or Opt Out response to the VTN.
 
-In order to track what happens after, there is a Reports mechanism in place that allows the VEN and the VTN to agree on what data should be reported.
+In order to track what happens after, there is a Reports mechanism in place that allows the VEN and the VTN to agree on what data should be reported, and to report this data at a requested interval.
+
+Although multiple transport formats are supported (HTTP and XMPP), OpenADR is designed for only the VTN to be public-accessible, with the VENs possibly being behind NAT or firewalls. All communications are therefore initiated by the client (VEN), and the server can request additional messages from the client in its response to the original request.
 
 
 .. _registration:
@@ -19,7 +21,7 @@ In order to track what happens after, there is a Reports mechanism in place that
 Registration
 ============
 
-Here is the registration page
+(Information on the Registration procedures)
 
 
 
@@ -28,11 +30,11 @@ Here is the registration page
 Events
 ======
 
-This is the registration
+(Information on the Events procedures)
 
 .. _openadr_reports:
 
 Reports
 =======
 
-This is the reports
+(Information on the Reports procedures)

+ 72 - 2
docs/representations.rst

@@ -16,7 +16,7 @@ The following general principles have been applied to representing OpenADR objec
 - For all properties, the ``oadr*`` and ``Ei*`` prefixes have been stripped away. For example: ``eiResponse`` becomes ``response`` and ``oadrResponse`` becomes ``response``.
 - OpenADR timestamps are converted to Python ``datetime.datetime`` objects.
 - OpenADR time intervals are converted to Python ``datetime.timedelta`` objects.
-- Properties that might have more than 1 copy in the XML representation are put in a list, even if there is just one. This list will be identified by the pluralized version of the originals property name. For example:
+- Properties that might have more than 1 copy in the XML representation are put in a list, even if there is just one. This list will be identified by the pluralized version of the original property name. For example:
 
 .. code-block:: xml
 
@@ -35,7 +35,7 @@ Will become:
 
 - The messages are usually used as a ``message_name, message_payload`` tuple. The message name is kept, for instance, ``oadrCanceledOpt``, and the payload is given as a dict.
 
-Below is an alphabetized overview of all messages with their XML and Python representations.
+Below is an alphabetized overview of all payloads with their XML and Python representations.
 
 .. _oadrCanceledOpt:
 
@@ -1294,3 +1294,73 @@ pyOpenADR representation:
                   'response_code': 200,
                   'response_description': 'OK'},
      'ven_id': '123ABC'}
+
+
+.. _oadrUpdateReport:
+
+oadrUpdateReport
+================
+
+This message contains a report.
+
+OpenADR payload:
+
+.. code-block:: xml
+
+    <?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>bfbaaa469c</pyld:requestID>
+          <oadrReport>
+            <ei:eiReportID>z4edcf6f9d</ei:eiReportID>
+            <oadrReportDescription xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06">
+              <ei:rID>s1167debd8</ei:rID>
+              <ei:reportSubject>
+                <ei:venID>123ABC</ei:venID>
+                <ei:venID>DEF456</ei:venID>
+              </ei:reportSubject>
+              <ei:reportDataSource>
+                <ei:venID>123ABC</ei:venID>
+              </ei:reportDataSource>
+              <ei:reportType>x-resourceStatus</ei:reportType>
+              <ei:readingType>x-RMS</ei:readingType>
+              <emix:marketContext>http://localhost</emix:marketContext>
+              <oadrSamplingRate>
+                <oadrMinPeriod>PT1M</oadrMinPeriod>
+                <oadrMaxPeriod>PT2M</oadrMaxPeriod>
+                <oadrOnChange>false</oadrOnChange>
+              </oadrSamplingRate>
+            </oadrReportDescription>
+            <ei:reportRequestID>m04fa486ef</ei:reportRequestID>
+            <ei:reportSpecifierID>w5fdcab8d0</ei:reportSpecifierID>
+            <ei:reportName>TELEMETRY_USAGE</ei:reportName>
+            <ei:createdDateTime>2020-07-10T09:24:38.606626Z</ei:createdDateTime>
+          </oadrReport>
+          <ei:venID>123ABC</ei:venID>
+        </oadrUpdateReport>
+      </oadrSignedObject>
+    </oadrPayload>
+
+pyOpenADR representation:
+
+.. code-block:: python3
+
+    {'reports': [{'created_date_time': datetime.datetime(2020, 7, 10, 9, 24, 38, 606626, tzinfo=datetime.timezone.utc),
+                  'report_descriptions': [{'market_context': 'http://localhost',
+                                           'r_id': 's1167debd8',
+                                           'reading_type': 'x-RMS',
+                                           'report_data_sources': [{'ven_id': '123ABC'}],
+                                           'report_subjects': [{'ven_id': '123ABC'},
+                                                               {'ven_id': 'DEF456'}],
+                                           'report_type': 'x-resourceStatus',
+                                           'sampling_rate': {'max_period': datetime.timedelta(seconds=120),
+                                                             'min_period': datetime.timedelta(seconds=60),
+                                                             'on_change': False}}],
+                  'report_id': 'z4edcf6f9d',
+                  'report_name': 'TELEMETRY_USAGE',
+                  'report_request_id': 'm04fa486ef',
+                  'report_specifier_id': 'w5fdcab8d0'}],
+     'request_id': 'bfbaaa469c',
+     'ven_id': '123ABC'}
+

+ 19 - 0
docs/roadmap.rst

@@ -0,0 +1,19 @@
+.. _roadmap:
+
+==========================
+Project Status and Roadmap
+==========================
+
+pyOpenADR is under development. The current version is |release|.
+
+Upcoming releases
+-----------------
+
+======= ================================== ====================
+Version Main features                      Target date
+======= ================================== ====================
+0.4.0   Implement XML message signing      September 15th, 2020
+0.5.0   Implement reporting                October 1st, 2020
+0.6.0   Implement XMPP transport           November 1st, 2020
+1.0.0   Certification by OpenADR Alliance  T.B.A.
+======= ================================== ====================

+ 82 - 4
docs/server.rst

@@ -4,8 +4,7 @@
 Server
 ======
 
-The page contains all information about pyOpenADR Server API.
-
+If you are implementing an OpenADR Server ("Virtual Top Node") using pyOpenADR, read this page.
 
 .. _server_registration:
 
@@ -14,7 +13,12 @@ Registration
 
 If a client (VEN) wants to register for the first time, it will go through a Registration procedure.
 
-The client will send a :ref:`oadrQueryRegistration` message. The server will respond with a :ref:`oadrCreatedPartyRegistration` message containing a list of its capabilities, notable the implemented OpenADR protocol versions and the available Transport Mechanisms (HTTP and/or XMPP).
+.. admonition:: Implementation Checklist
+
+    1. Create a handler that decides what to do with new registrations, based on their ``venID``.
+
+
+The client will send a :ref:`oadrQueryRegistration` message. The server will respond with a :ref:`oadrCreatedPartyRegistration` message containing a list of its capabilities, notably the implemented OpenADR protocol versions and the available Transport Mechanisms (HTTP and/or XMPP).
 
 The client will then usually send a :ref:`oadrCreatePartyRegistration` message, in which it registers to a specific OpenADR version and Transport Method. The server must then decide what it wants to do with this registration.
 
@@ -58,10 +62,84 @@ The server (VTN) is expected to know when it needs to inform the clients (VENs)
 
 The VTN must determine when VENs are relevant and which Events to send to them. The next time the VEN polls for new messages (using a :ref:`oadrPoll` or :ref:`oadrRequestEvent` message), it will send the Event in a :ref:`oadrDistributeEvent` message to the client. The client will then evaluate whether or not it indends to comply with the request, and respond with an :ref:`oadrCreatedEvent` message containing an optStatus of ``'optIn'`` or ``'optOut'``.
 
-In your application, the creation of Events is completely up to you. PyOpenADR will only call your ``on_poll`` handler with a ``ven_id``. This handler must be able to either retrieve the next event for this VEN out of some storage or queue, or make up the Event in real time.
+.. admonition:: Implementation Checklist
+
+    In your application, the creation of Events is completely up to you. PyOpenADR will only call your ``on_poll`` handler with a ``ven_id``. This handler must be able to either retrieve the next event for this VEN out of some storage or queue, or make up the Event in real time.
+
+    - ``on_created_event(payload)`` handler is called whenever the VEN sends an :ref:`oadrCreatedEvent` message, probably informing you of what they intend to do with the event you gave them.
+    - ``on_request_event(ven_id)``: this should return the next event (if any) that you have for the VEN. If you return ``None``, a blank :ref:`oadrResponse` will be returned to the VEN.
+    - ``on_request_report(ven_id)``: this should return then next report (if any) that you have for the VEN. If you return None, a blank :ref:`oadrResponse` will be returned to the VEN.
+    - ``on_poll(ven_id)``: this should return the next message in line, which is usually either a new :ref:`oadrUpdatedReport` or a :ref:`oadrDistributeEvent` message.
+
+
+The Event consists of three main sections:
+
+1. A time period for when this event is supposed to be active
+2. A list of Targets to which the Event applies. This can be the VEN as a whole, or specific groups, assets, geographic areas, et cetera that this VEN represents.
+3. A list of Signals, which form the content of the Event. This can be price signals, load reduction signals, et cetera. Each signal has a name, a type, multiple Intervals that contain the relative start times, and some payload value for the client to interpret.
+
 
 
 .. _server_reports:
 
 Reports
 =======
+
+Reporting is probably the most complicated of interactions within OpenADR. It involves the following steps:
+
+1. Party A makes its reporting capabilities known to party B using a :ref:`oadrRegisterReport` message.
+2. Party B responds with an :ref:`oadrRegisteredReport` message, optionally including an :ref:`oadrReportRequest` section that tells party A which party B is interested in.
+3. Party A reponds with an oadrCreatedReport message telling party B that it will periodically generate the reports.
+
+This ceremony is performed once with the VTN as party A and once with the VEN as party A.
+
+The VEN party can collect the reports it requested from the VTN using either the :ref:`oadrPoll` or :ref:`oadrRequestReport` messages. The VTN will respond with an :ref:`oadrUpdateReport` message containing the actual report. The VEN should then respond with a :ref:`oadrUpdatedReport` message.
+
+The VEN should actively supply the reports to the VTN using :ref:`oadrUpdateReport` messages, to which the VTN will respond with :ref:`oadrUpdatedReport` messages.
+
+.. admonition:: Implementation Checklist
+
+    To benefit from the automatic reporting engine in pyOpenADR, you should implement the following items yourself:
+
+    1. Configure the OpenADRServer() instance with your reporting capabilities and requirements
+    2. Implement a handlers that can retrieve the reports from your backend system
+    3. Implement a handler that deal with reports that come in from the clients
+
+
+.. _server_implement:
+
+Things you should implement
+===========================
+
+You should implement the following handlers:
+
+- ``on_poll(ven_id)``
+- ``on_request_event(ven_id)``
+- ``on_request_report(payload)``
+- ``on_create_party_registration(payload)``
+
+.. _server_meta:
+
+Non-OpenADR signals from the server
+===================================
+
+The pyOpenADR Server can call the following handlers, which are not part of the regular openADR communication flow, but can help you develop a more robust event-driven system:
+
+- ``on_ven_online(ven_id)``: called when a VEN sends an :ref:`oadrPoll`, :ref:`oadrRequestEvent` or :ref:`oadrRequestReport` message after it had been offline before.
+- ``on_ven_offline(ven_id)``: called when a VEN misses 3 consecutive poll intervals (configurable).
+
+Example implementation:
+
+.. code-block:: python3
+
+    from pyopenadr import OpenADRServer
+
+    server = OpenADRServer(vtn_id='MyVTN')
+    server.add_handler('on_ven_online', on_ven_online)
+    server.add_handler('on_ven_offline', on_ven_offline)
+
+    async def on_ven_online(ven_id):
+        print(f"VEN {ven_id} is now online again!")
+
+    async def on_ven_offline(ven_id):
+        print(f"VEN {ven_id} has gone AWOL")

+ 31 - 10
pyopenadr/client.py

@@ -29,6 +29,7 @@ from http import HTTPStatus
 from apscheduler.schedulers.asyncio import AsyncIOScheduler
 import asyncio
 from asyncio import iscoroutine
+from functools import partial
 
 MEASURANDS = {'power_real': 'power_quantity',
               'power_reactive': 'power_quantity',
@@ -42,14 +43,18 @@ class OpenADRClient:
     Main client class. Most of these methods will be called automatically, but
     you can always choose to call them manually.
     """
-    def __init__(self, ven_name, vtn_url, debug=False):
+    def __init__(self, ven_name, vtn_url, debug=False, cert=None, key=None, passphrase=None, verification_cert=None):
         """
         Initializes a new OpenADR Client (Virtual End Node)
 
         :param str ven_name: The name for this VEN
         :param str vtn_url: The URL of the VTN (Server) to connect to
         :param bool debug: Whether or not to print debugging messages
+        :param str cert: The path to a PEM-formatted Certificate file to use for signing messages
+        :param str key: The path to a PEM-formatted Private Key file to use for signing messages
+        :param str verification_cert: The path to a PEM-formatted Certificate file to use for verifying incoming messages.
         """
+
         self.ven_name = ven_name
         self.vtn_url = vtn_url
         self.ven_id = None
@@ -61,6 +66,23 @@ class OpenADRClient:
         self.scheduler = AsyncIOScheduler()
         self.client_session = aiohttp.ClientSession()
 
+        if cert and key:
+            with open(cert, 'rb') as file:
+                cert = file.read()
+            with open(key, 'rb') as file:
+                key = file.read()
+
+        self._create_message = partial(create_message,
+                                       cert=cert,
+                                       key=key,
+                                       passphrase=passphrase)
+        if verification_cert:
+            with open(verification_cert, "rb") as file:
+                verification_cert = file.read()
+        self._parse_message = partial(parse_message,
+                                      cert=verification_cert)
+
+
     async def run(self):
         """
         Run the client in full-auto mode.
@@ -157,7 +179,7 @@ class OpenADRClient:
         """
         request_id = new_request_id()
         service = 'EiRegisterParty'
-        message = create_message('oadrQueryRegistration', request_id=request_id)
+        message = self._create_message('oadrQueryRegistration', request_id=request_id)
         response_type, response_payload = await self._perform_request(service, message)
         return response_type, response_payload
 
@@ -186,7 +208,7 @@ class OpenADRClient:
                    'transport_address': transport_address}
         if ven_id:
             payload['ven_id'] = ven_id
-        message = create_message('oadrCreatePartyRegistration', request_id=new_request_id(), **payload)
+        message = self._create_message('oadrCreatePartyRegistration', request_id=new_request_id(), **payload)
         response_type, response_payload = await self._perform_request(service, message)
         if response_payload['response']['response_code'] != 200:
             status_code = response_payload['response']['response_code']
@@ -209,7 +231,7 @@ class OpenADRClient:
         payload = {'request_id': new_request_id(),
                    'ven_id': self.ven_id,
                    'reply_limit': reply_limit}
-        message = create_message('oadrRequestEvent', **payload)
+        message = self._create_message('oadrRequestEvent', **payload)
         service = 'EiEvent'
         response_type, response_payload = await self._perform_request(service, message)
         return response_type, response_payload
@@ -229,7 +251,7 @@ class OpenADRClient:
                                         'event_id': event_id,
                                         'modification_number': modification_number,
                                         'opt_type': opt_type}]}
-        message = create_message('oadrCreatedEvent', **payload)
+        message = self._create_message('oadrCreatedEvent', **payload)
         response_type, response_payload = await self._perform_request(service, message)
         return response_type, response_payload
 
@@ -244,7 +266,7 @@ class OpenADRClient:
                    'reports': self.reports}
 
         service = 'EiReport'
-        message = create_message('oadrRegisterReport', **payload)
+        message = self._create_message('oadrRegisterReport', **payload)
         response_type, response_payload = await self._perform_request(service, message)
 
         # Remember which reports the VTN is interested in
@@ -259,7 +281,7 @@ class OpenADRClient:
         Request the next available message from the Server. This coroutine is called automatically.
         """
         service = 'OadrPoll'
-        message = create_message('oadrPoll', ven_id=self.ven_id)
+        message = self._create_message('oadrPoll', ven_id=self.ven_id)
         response_type, response_payload = await self._perform_request(service, message)
         return response_type, response_payload
 
@@ -293,7 +315,7 @@ class OpenADRClient:
                   'created_date_time': datetime.now(timezone.utc)}
 
         service = 'EiReport'
-        message = create_message('oadrUpdateReport', report)
+        message = self._create_message('oadrUpdateReport', report)
         response_type, response_payload = self._perform_request(service, message)
 
         # We might get a oadrCancelReport message in this thing:
@@ -311,7 +333,7 @@ class OpenADRClient:
             content = await req.read()
             if self.debug:
                 print(content.decode('utf-8'))
-        return parse_message(content)
+        return self._parse_message(content)
 
     async def _on_event(self, message):
         if self.debug:
@@ -351,4 +373,3 @@ class OpenADRClient:
         else:
             print(f"No handler implemented for message type {response_type}, ignoring.")
         await self._poll()
-

+ 70 - 8
pyopenadr/messaging.py

@@ -14,40 +14,102 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from lxml import etree
 import xmltodict
-from jinja2 import Environment, PackageLoader, select_autoescape
+from jinja2 import Environment, PackageLoader
+from signxml import XMLSigner, XMLVerifier, methods
+from uuid import uuid4
+from lxml.etree import Element
 
 from .utils import *
-from .signature import *
 from .preflight import preflight_message
 
-def parse_message(data):
+SIGNER = XMLSigner(method=methods.detached,
+                   c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#")
+VERIFIER = XMLVerifier()
+
+def parse_message(data, cert=None):
     """
     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()
+    if cert:
+        tree = etree.fromstring(ensure_bytes(data))
+        VERIFIER.verify(tree, x509_cert=cert, expect_references=2)
+        _verify_replay_protect(message_dict)
     return message_type, normalize_dict(message_payload)
 
-def create_message(message_type, **message_payload):
+def create_message(message_type, cert=None, key=None, passphrase=None, **message_payload):
     """
-    This creates an OpenADR message. This consists
+    Create and optionally sign an OpenADR message. Returns an XML string.
     """
     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)
-
+    signed_object = flatten_xml(TEMPLATES.get_template(f'{message_type}.xml').render(**message_payload))
     envelope = TEMPLATES.get_template('oadrPayload.xml')
+    if cert and key:
+        tree = etree.fromstring(signed_object)
+        signature_tree = SIGNER.sign(tree,
+                                     key=key,
+                                     cert=cert,
+                                     passphrase=ensure_bytes(passphrase),
+                                     reference_uri="#oadrSignedObject",
+                                     signature_properties=_create_replay_protect())
+        signature = etree.tostring(signature_tree).decode('utf-8')
+    else:
+        signature = None
+
     msg = envelope.render(template=f'{message_type}',
                           signature=signature,
                           signed_object=signed_object)
     return msg
 
+def _create_replay_protect():
+    dt_element = Element("{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}timestamp")
+    dt_element.text = datetimeformat(datetime.now(timezone.utc))
+
+    nonce_element = Element("{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}nonce")
+    nonce_element.text = uuid4().hex
+
+    el = Element("{http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties}ReplayProtect",
+                 nsmap={'dsp': 'http://openadr.org/oadr-2.0b/2012/07/xmldsig-properties'},
+                 attrib={'Id': 'myid', 'Target': '#mytarget'})
+    el.append(dt_element)
+    el.append(nonce_element)
+    return el
+
+def _verify_replay_protect(message_dict):
+    try:
+        ts = message_dict['oadrPayload']['Signature']['Object']['SignatureProperties']['SignatureProperty']['ReplayProtect']['timestamp']
+        nonce = message_dict['oadrPayload']['Signature']['Object']['SignatureProperties']['SignatureProperty']['ReplayProtect']['nonce']
+    except KeyError:
+        raise ValueError("Missing ReplayProtect")
+    else:
+        timestamp = datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S.%f%z")
+        if timestamp < datetime.now(timezone.utc) - REPLAY_PROTECT_MAX_TIME_DELTA:
+            raise ValueError("Message is too old")
+        elif (timestamp, nonce) in NONCE_CACHE:
+            raise ValueError("This combination of timestamp and nonce was already used")
+    _update_nonce_cache(timestamp, nonce)
+
+def _update_nonce_cache(timestamp, nonce):
+    for timestamp, nonce in list(NONCE_CACHE):
+        if timestamp < datetime.now(timezone.utc) - REPLAY_PROTECT_MAX_TIME_DELTA:
+            NONCE_CACHE.remove((timestamp, nonce))
+    NONCE_CACHE.add((timestamp, nonce))
+
+# Replay protect settings
+REPLAY_PROTECT_MAX_TIME_DELTA = timedelta(seconds=5)
+NONCE_CACHE = set()
+
+
 # Settings for jinja2
 TEMPLATES = Environment(loader=PackageLoader('pyopenadr', 'templates'))
 TEMPLATES.filters['datetimeformat'] = datetimeformat
 TEMPLATES.filters['timedeltaformat'] = timedeltaformat
 TEMPLATES.filters['booleanformat'] = booleanformat
+TEMPLATES.trim_blocks = True
+TEMPLATES.lstrip_blocks = True
 
 # Settings for xmltodict
 NAMESPACES = {

+ 1 - 3
pyopenadr/preflight.py

@@ -23,7 +23,7 @@ import warnings
 
 def preflight_message(message_type, message_payload):
     if f'preflight_{message_type}' in globals():
-        message_payload = globals()[f'preflight_{message_type}'](message_payload)
+        globals()[f'preflight_{message_type}'](message_payload)
     return message_type, message_payload
 
 def preflight_oadrDistributeEvent(message_payload):
@@ -36,8 +36,6 @@ def preflight_oadrDistributeEvent(message_payload):
         for signal in event['event_signals']:
             signal_durations.append(sum([parse_duration(i['duration']) for i in signal['intervals']], timedelta(seconds=0)))
 
-
-
         if not all([d==active_period_duration for d in signal_durations]):
             if not all([d==signal_durations[0] for d in signal_durations]):
                 raise ValueError("The different EventSignals have different total durations. Please correct this.")

+ 17 - 2
pyopenadr/server.py

@@ -15,7 +15,9 @@
 # limitations under the License.
 
 from aiohttp import web
-from pyopenadr.service import EventService, PollService, RegistrationService, ReportService, OptService
+from pyopenadr.service import EventService, PollService, RegistrationService, ReportService, OptService, VTNService
+from pyopenadr.messaging import create_message, parse_message
+from functools import partial
 
 class OpenADRServer:
     _MAP = {'on_created_event': EventService,
@@ -33,7 +35,7 @@ class OpenADRServer:
            'on_create_party_registration': RegistrationService,
            'on_cancel_party_registration': RegistrationService}
 
-    def __init__(self, vtn_id):
+    def __init__(self, vtn_id, cert=None, key=None, passphrase=None, verification_cert=None):
         self.app = web.Application()
         self.services = {'event_service': EventService(vtn_id),
                          'report_service': ReportService(vtn_id),
@@ -41,6 +43,19 @@ class OpenADRServer:
                          'opt_service': OptService(vtn_id),
                          'registration_service': RegistrationService(vtn_id)}
         self.app.add_routes([web.post(f"/OpenADR2/Simple/2.0b/{s.__service_name__}", s.handler) for s in self.services.values()])
+
+        # Configure message signing
+        if cert and key:
+            with open(cert, "rb") as file:
+                cert = file.read()
+            with open(key, "rb") as file:
+                key = file.read()
+        if verification_cert:
+            with open(verification_cert, "rb") as file:
+                verification_cert = file.read()
+        VTNService._create_message = partial(create_message, cert=cert, key=key, passphrase=passphrase)
+        VTNService._parse_message = partial(parse_message, cert=verification_cert)
+
         self.__setattr__ = self.add_handler
 
     def run(self):

+ 6 - 5
pyopenadr/service/vtn_service.py

@@ -39,7 +39,7 @@ class VTNService:
         """
         content = await request.read()
         print(f"Received: {content.decode('utf-8')}")
-        message_type, message_payload = parse_message(content)
+        message_type, message_payload = self._parse_message(content)
         print(f"Interpreted message: {message_type}: {message_payload}")
 
         if message_type in self.handlers:
@@ -48,16 +48,17 @@ class VTNService:
             response_payload['vtn_id'] = self.vtn_id
 
             # Create the XML response
-            msg = create_message(response_type, **response_payload)
+            msg = self._create_message(response_type, **response_payload)
             response = web.Response(text=msg,
                                     status=HTTPStatus.OK,
                                     content_type='application/xml')
 
         else:
-            template = templates.get_template('oadrResponse.xml')
+            msg = self._create_message('oadrResponse',
+                                       status_code=errorcodes.COMPLIANCE_ERROR,
+                                       status_description=f'A message of type {message_type} should not be sent to this endpoint')
             response = web.Response(
-                text=template.render(status_code=errorcodes.COMPLIANCE_ERROR,
-                                     status_description=f'A message of type {message_type} should not be sent to this endpoint'),
+                text=msg,
                 status=HTTPStatus.BAD_REQUEST,
                 content_type='application/xml')
         print(f"Sending {response.text}")

+ 0 - 166
pyopenadr/signature.py

@@ -1,166 +0,0 @@
-# SPDX-License-Identifier: Apache-2.0
-
-# Copyright 2020 Contributors to OpenLEADR
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-#     http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-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

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

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

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

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

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

@@ -1,8 +0,0 @@
-  <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>

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

@@ -1,10 +0,0 @@
-    <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>

+ 15 - 0
pyopenadr/utils.py

@@ -50,6 +50,13 @@ def indent_xml(message):
             indent = indent + INDENT_SIZE
     return "\n".join(lines)
 
+def flatten_xml(message):
+    lines = [line.strip() for line in message.split("\n") if line.strip() != ""]
+    for line in lines:
+        line = re.sub(r'\n', '', line)
+        line = re.sub(r'\s\s+', ' ', line)
+    return "".join(lines)
+
 def normalize_dict(ordered_dict):
     """
     Convert the OrderedDict to a regular dict, snake_case the key names, and promote uniform lists.
@@ -331,3 +338,11 @@ def booleanformat(value):
         return value
     else:
         raise ValueError("A boolean value must be provided.")
+
+def ensure_bytes(obj):
+    if isinstance(obj, bytes):
+        return obj
+    if isinstance(obj, str):
+        return bytes(obj, 'utf-8')
+    else:
+        raise TypeError("Must be bytes or str")

+ 318 - 0
schema/xmldsig-core-schema.xsd

@@ -0,0 +1,318 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE schema
+  PUBLIC "-//W3C//DTD XMLSchema 200102//EN" "http://www.w3.org/2001/XMLSchema.dtd"
+ [
+   <!ATTLIST schema 
+     xmlns:ds CDATA #FIXED "http://www.w3.org/2000/09/xmldsig#">
+   <!ENTITY dsig 'http://www.w3.org/2000/09/xmldsig#'>
+   <!ENTITY % p ''>
+   <!ENTITY % s ''>
+  ]>
+
+<!-- Schema for XML Signatures
+    http://www.w3.org/2000/09/xmldsig#
+    $Revision: 1.1 $ on $Date: 2002/02/08 20:32:26 $ by $Author: reagle $
+
+    Copyright 2001 The Internet Society and W3C (Massachusetts Institute
+    of Technology, Institut National de Recherche en Informatique et en
+    Automatique, Keio University). All Rights Reserved.
+    http://www.w3.org/Consortium/Legal/
+
+    This document is governed by the W3C Software License [1] as described
+    in the FAQ [2].
+
+    [1] http://www.w3.org/Consortium/Legal/copyright-software-19980720
+    [2] http://www.w3.org/Consortium/Legal/IPR-FAQ-20000620.html#DTD
+-->
+
+
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+        targetNamespace="http://www.w3.org/2000/09/xmldsig#"
+        version="0.1" elementFormDefault="qualified"> 
+
+<!-- Basic Types Defined for Signatures -->
+
+<simpleType name="CryptoBinary">
+  <restriction base="base64Binary">
+  </restriction>
+</simpleType>
+
+<!-- Start Signature -->
+
+<element name="Signature" type="ds:SignatureType"/>
+<complexType name="SignatureType">
+  <sequence> 
+    <element ref="ds:SignedInfo"/> 
+    <element ref="ds:SignatureValue"/> 
+    <element ref="ds:KeyInfo" minOccurs="0"/> 
+    <element ref="ds:Object" minOccurs="0" maxOccurs="unbounded"/> 
+  </sequence>  
+  <attribute name="Id" type="ID" use="optional"/>
+</complexType>
+
+  <element name="SignatureValue" type="ds:SignatureValueType"/> 
+  <complexType name="SignatureValueType">
+    <simpleContent>
+      <extension base="base64Binary">
+        <attribute name="Id" type="ID" use="optional"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+
+<!-- Start SignedInfo -->
+
+<element name="SignedInfo" type="ds:SignedInfoType"/>
+<complexType name="SignedInfoType">
+  <sequence> 
+    <element ref="ds:CanonicalizationMethod"/> 
+    <element ref="ds:SignatureMethod"/> 
+    <element ref="ds:Reference" maxOccurs="unbounded"/> 
+  </sequence>  
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+  <element name="CanonicalizationMethod" type="ds:CanonicalizationMethodType"/> 
+  <complexType name="CanonicalizationMethodType" mixed="true">
+    <sequence>
+      <any namespace="##any" minOccurs="0" maxOccurs="unbounded"/>
+      <!-- (0,unbounded) elements from (1,1) namespace -->
+    </sequence>
+    <attribute name="Algorithm" type="anyURI" use="required"/> 
+  </complexType>
+
+  <element name="SignatureMethod" type="ds:SignatureMethodType"/>
+  <complexType name="SignatureMethodType" mixed="true">
+    <sequence>
+      <element name="HMACOutputLength" minOccurs="0" type="ds:HMACOutputLengthType"/>
+      <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+      <!-- (0,unbounded) elements from (1,1) external namespace -->
+    </sequence>
+    <attribute name="Algorithm" type="anyURI" use="required"/> 
+  </complexType>
+
+<!-- Start Reference -->
+
+<element name="Reference" type="ds:ReferenceType"/>
+<complexType name="ReferenceType">
+  <sequence> 
+    <element ref="ds:Transforms" minOccurs="0"/> 
+    <element ref="ds:DigestMethod"/> 
+    <element ref="ds:DigestValue"/> 
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+  <attribute name="URI" type="anyURI" use="optional"/> 
+  <attribute name="Type" type="anyURI" use="optional"/> 
+</complexType>
+
+  <element name="Transforms" type="ds:TransformsType"/>
+  <complexType name="TransformsType">
+    <sequence>
+      <element ref="ds:Transform" maxOccurs="unbounded"/>  
+    </sequence>
+  </complexType>
+
+  <element name="Transform" type="ds:TransformType"/>
+  <complexType name="TransformType" mixed="true">
+    <choice minOccurs="0" maxOccurs="unbounded"> 
+      <any namespace="##other" processContents="lax"/>
+      <!-- (1,1) elements from (0,unbounded) namespaces -->
+      <element name="XPath" type="string"/> 
+    </choice>
+    <attribute name="Algorithm" type="anyURI" use="required"/> 
+  </complexType>
+
+<!-- End Reference -->
+
+<element name="DigestMethod" type="ds:DigestMethodType"/>
+<complexType name="DigestMethodType" mixed="true"> 
+  <sequence>
+    <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+  </sequence>    
+  <attribute name="Algorithm" type="anyURI" use="required"/> 
+</complexType>
+
+<element name="DigestValue" type="ds:DigestValueType"/>
+<simpleType name="DigestValueType">
+  <restriction base="base64Binary"/>
+</simpleType>
+
+<!-- End SignedInfo -->
+
+<!-- Start KeyInfo -->
+
+<element name="KeyInfo" type="ds:KeyInfoType"/> 
+<complexType name="KeyInfoType" mixed="true">
+  <choice maxOccurs="unbounded">     
+    <element ref="ds:KeyName"/> 
+    <element ref="ds:KeyValue"/> 
+    <element ref="ds:RetrievalMethod"/> 
+    <element ref="ds:X509Data"/> 
+    <element ref="ds:PGPData"/> 
+    <element ref="ds:SPKIData"/>
+    <element ref="ds:MgmtData"/>
+    <any processContents="lax" namespace="##other"/>
+    <!-- (1,1) elements from (0,unbounded) namespaces -->
+  </choice>
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+  <element name="KeyName" type="string"/>
+  <element name="MgmtData" type="string"/>
+
+  <element name="KeyValue" type="ds:KeyValueType"/> 
+  <complexType name="KeyValueType" mixed="true">
+   <choice>
+     <element ref="ds:DSAKeyValue"/>
+     <element ref="ds:RSAKeyValue"/>
+     <any namespace="##other" processContents="lax"/>
+   </choice>
+  </complexType>
+
+  <element name="RetrievalMethod" type="ds:RetrievalMethodType"/> 
+  <complexType name="RetrievalMethodType">
+    <sequence>
+      <element ref="ds:Transforms" minOccurs="0"/> 
+    </sequence>  
+    <attribute name="URI" type="anyURI"/>
+    <attribute name="Type" type="anyURI" use="optional"/>
+  </complexType>
+
+<!-- Start X509Data -->
+
+<element name="X509Data" type="ds:X509DataType"/> 
+<complexType name="X509DataType">
+  <sequence maxOccurs="unbounded">
+    <choice>
+      <element name="X509IssuerSerial" type="ds:X509IssuerSerialType"/>
+      <element name="X509SKI" type="base64Binary"/>
+      <element name="X509SubjectName" type="string"/>
+      <element name="X509Certificate" type="base64Binary"/>
+      <element name="X509CRL" type="base64Binary"/>
+      <any namespace="##other" processContents="lax"/>
+    </choice>
+  </sequence>
+</complexType>
+
+<complexType name="X509IssuerSerialType"> 
+  <sequence> 
+    <element name="X509IssuerName" type="string"/> 
+    <element name="X509SerialNumber" type="integer"/> 
+  </sequence>
+</complexType>
+
+<!-- End X509Data -->
+
+<!-- Begin PGPData -->
+
+<element name="PGPData" type="ds:PGPDataType"/> 
+<complexType name="PGPDataType"> 
+  <choice>
+    <sequence>
+      <element name="PGPKeyID" type="base64Binary"/> 
+      <element name="PGPKeyPacket" type="base64Binary" minOccurs="0"/> 
+      <any namespace="##other" processContents="lax" minOccurs="0"
+       maxOccurs="unbounded"/>
+    </sequence>
+    <sequence>
+      <element name="PGPKeyPacket" type="base64Binary"/> 
+      <any namespace="##other" processContents="lax" minOccurs="0"
+       maxOccurs="unbounded"/>
+    </sequence>
+  </choice>
+</complexType>
+
+<!-- End PGPData -->
+
+<!-- Begin SPKIData -->
+
+<element name="SPKIData" type="ds:SPKIDataType"/> 
+<complexType name="SPKIDataType">
+  <sequence maxOccurs="unbounded">
+    <element name="SPKISexp" type="base64Binary"/>
+    <any namespace="##other" processContents="lax" minOccurs="0"/>
+  </sequence>
+</complexType> 
+
+<!-- End SPKIData -->
+
+<!-- End KeyInfo -->
+
+<!-- Start Object (Manifest, SignatureProperty) -->
+
+<element name="Object" type="ds:ObjectType"/> 
+<complexType name="ObjectType" mixed="true">
+  <sequence minOccurs="0" maxOccurs="unbounded">
+    <any namespace="##any" processContents="lax"/>
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+  <attribute name="MimeType" type="string" use="optional"/> <!-- add a grep facet -->
+  <attribute name="Encoding" type="anyURI" use="optional"/> 
+</complexType>
+
+<element name="Manifest" type="ds:ManifestType"/> 
+<complexType name="ManifestType">
+  <sequence>
+    <element ref="ds:Reference" maxOccurs="unbounded"/> 
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+<element name="SignatureProperties" type="ds:SignaturePropertiesType"/> 
+<complexType name="SignaturePropertiesType">
+  <sequence>
+    <element ref="ds:SignatureProperty" maxOccurs="unbounded"/> 
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+   <element name="SignatureProperty" type="ds:SignaturePropertyType"/> 
+   <complexType name="SignaturePropertyType" mixed="true">
+     <choice maxOccurs="unbounded">
+       <any namespace="##other" processContents="lax"/>
+       <!-- (1,1) elements from (1,unbounded) namespaces -->
+     </choice>
+     <attribute name="Target" type="anyURI" use="required"/> 
+     <attribute name="Id" type="ID" use="optional"/> 
+   </complexType>
+
+<!-- End Object (Manifest, SignatureProperty) -->
+
+<!-- Start Algorithm Parameters -->
+
+<simpleType name="HMACOutputLengthType">
+  <restriction base="integer"/>
+</simpleType>
+
+<!-- Start KeyValue Element-types -->
+
+<element name="DSAKeyValue" type="ds:DSAKeyValueType"/>
+<complexType name="DSAKeyValueType">
+  <sequence>
+    <sequence minOccurs="0">
+      <element name="P" type="ds:CryptoBinary"/>
+      <element name="Q" type="ds:CryptoBinary"/>
+    </sequence>
+    <element name="G" type="ds:CryptoBinary" minOccurs="0"/>
+    <element name="Y" type="ds:CryptoBinary"/>
+    <element name="J" type="ds:CryptoBinary" minOccurs="0"/>
+    <sequence minOccurs="0">
+      <element name="Seed" type="ds:CryptoBinary"/>
+      <element name="PgenCounter" type="ds:CryptoBinary"/>
+    </sequence>
+  </sequence>
+</complexType>
+
+<element name="RSAKeyValue" type="ds:RSAKeyValueType"/>
+<complexType name="RSAKeyValueType">
+  <sequence>
+    <element name="Modulus" type="ds:CryptoBinary"/> 
+    <element name="Exponent" type="ds:CryptoBinary"/> 
+  </sequence>
+</complexType> 
+
+<!-- End KeyValue Element-types -->
+
+<!-- End Signature -->
+
+</schema>

+ 2 - 2
setup.py

@@ -28,7 +28,7 @@ setup(name="pyopenadr",
       description="Python library for dealing with OpenADR",
       long_description=long_description,
       long_description_content_type="text/markdown",
-      url="https://git.finetuned.nl/stan/pyopenadr",
+      url="https://finetuned.nl/pyopenadr",
       packages=['pyopenadr', 'pyopenadr.service'],
       include_package_data=True,
-      install_requires=['xmltodict', 'aiohttp', 'apscheduler', 'jinja2', 'cryptography'])
+      install_requires=['xmltodict', 'aiohttp', 'apscheduler', 'jinja2', 'signxml-openadr>2.8.0'])

+ 32 - 0
test/cert.pem

@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFiTCCA3GgAwIBAgIUIIpr283UlljB8eqbykYRd/j75h4wDQYJKoZIhvcNAQEL
+BQAwVDELMAkGA1UEBhMCTkwxEzARBgNVBAgMCkdlbGRlcmxhbmQxDzANBgNVBAcM
+BkFybmhlbTEfMB0GA1UECgwWT3BlbkxFQURSIE9yZ2FuaXphdGlvbjAeFw0yMDA5
+MDkwNzU3MTRaFw0zMDA5MDcwNzU3MTRaMFQxCzAJBgNVBAYTAk5MMRMwEQYDVQQI
+DApHZWxkZXJsYW5kMQ8wDQYDVQQHDAZBcm5oZW0xHzAdBgNVBAoMFk9wZW5MRUFE
+UiBPcmdhbml6YXRpb24wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM
+pEJj7sovh0EAQdCJW85cSPJ0UQZ137wGAP05sVjgh8k9kfyIbrsnUnhvByI/NHAQ
+nyXtJP8rvvakgsNj/YizzgYO8n6s69NGi0mnZCTV9gjUlt2HZ8v/UTkL4otEhXSw
+3r/B2vrTqfhNkJI/b9cJAaQGwLLc5TCC+NzukXUl4BtBv5js1Z29mnvsGHgvxzoA
+jcWBY52KHvAZvt0GsShyOje2E71gch/tMfKipqlNB2Cbmq9gFnGyJTJncbHoGMxr
+7is5v5bzJOXuJj/Eztbcj2rH31ltV1jBbAdWfcKGbMmTPNZTOvsDO6L2M0U5fiFR
+24fSfACd9IvvbiBgDIo9dMxdC2hJ3MwqJKK6L03ungDiXJyCtQqwgYtlSpfjI0tz
+kPs29pYtBqAdQEMOrdMcNn+94O6Axylr1fjTg3d41w6X7IOhfKxyG285fk6ad4DH
+3RrRRdjsO6LZUgSKzpxKGlGpcRKMgpMI360L44NJEh598W2whRjcozvNicqlWGze
+eu+zEeeFIta+9vfFxpI0aLLBUq3JZm2cdKSBfHGyBBV9ids0CfeIBFrwB0vVsF22
+UHg3t7LTfLaP4xlrRvlITzy2l0eJhDNPIWSXXA9Hl2+XQoYf4iYwY628loXPeyAd
+nXmMse0KbSJtUiuDqQOLuyF8aty4hNLzJ0K5w+OPyQIDAQABo1MwUTAdBgNVHQ4E
+FgQUjHzJIuGIAx5lr1bVjefd1Xi1/tYwHwYDVR0jBBgwFoAUjHzJIuGIAx5lr1bV
+jefd1Xi1/tYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAcCGc
+jjqT1LaJijJAY/EQxA/baqS3/d9dTH1A7khPTm9dtFAg1Nlrovhul4pi+yK9F4np
+iGgHsYx2IafuvTZ4DlcimtMfNRuH11BZsVQoBqJH3QPDa2s99zUnGN0XRYDgsh50
+9li9YW1PsNfcVX0YTkOMUgF9vEqiSnvpNnfdINEA/MJQysrERkfdzDO8pETQ0/cb
+VPpYStu4mJ1MQMQKuieYGtOkvzFWFT1aeQg1UMUATfBbWCC7vkbupMzhLiYFh+ZT
+ny17vhUMFMAr7Kt6cil+CavK9x88NRk4i1Jop1c/Nz6OlB67q4/Zhiz7NUKAUp0v
+iyWnelpg/jQ2tfFp6ThaIGX4Jgz1oa/w6QCCUCSwqZeuXl1sLZ8NyNsssGPLUuDt
+mbfmpVMoBleQB/J94P6QJtr9yCBWokUNrZGugmnrGuLR/FSBmq9C/hiuVnwM1uCM
+8yPPkjaD2pewXDzarrg9dEFd1G4WSv+YLj9uhuVILnS1GKzHBJtslYL7aVXvMlcx
+ER4rrcWunQBZAyWg8FqA+RlX30mDiHIDsZBl2/1t1yKCifhnMIgEx0kKk4KrD4CR
+f22dfslN4950FRhpA51rBdxKcJnw8XziucG8VGKnTONCF30IPDeT1eHj/th2m28S
+/Y2BqiYt4MxrrOmaEAaqupRYzH6FhG+jbgGIyVA=
+-----END CERTIFICATE-----

+ 0 - 1
test/conformance/test_conformance_002.py

@@ -20,7 +20,6 @@ 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
-from pyopenadr.signature import validate_message
 
 from pprint import pprint
 

+ 28 - 0
test/integration_tests/test_client_registration.py

@@ -24,11 +24,16 @@ import sqlite3
 import pytest
 from aiohttp import web
 
+import os
+
 SERVER_PORT = 8001
 VEN_NAME = 'myven'
 VEN_ID = '1234abcd'
 VTN_ID = "TestVTN"
 
+CERTFILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "cert.pem")
+KEYFILE =  os.path.join(os.path.dirname(os.path.dirname(__file__)), "key.pem")
+
 
 async def _on_create_party_registration(payload):
     registration_id = generate_id()
@@ -54,6 +59,18 @@ async def start_server():
     yield
     await runner.cleanup()
 
+@pytest.fixture
+async def start_server_with_signatures():
+    server = OpenADRServer(vtn_id=VTN_ID, cert=CERTFILE, key=KEYFILE, passphrase='openadr', verification_cert=CERTFILE)
+    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):
@@ -74,3 +91,14 @@ async def test_create_party_registration(start_server):
     assert response_payload['ven_id'] == VEN_ID
 
 
+@pytest.mark.asyncio
+async def test_create_party_registration_with_signatures(start_server_with_signatures):
+    client = OpenADRClient(ven_name=VEN_NAME,
+                           vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b",
+                           cert=CERTFILE, key=KEYFILE, passphrase='openadr', verification_cert=CERTFILE)
+
+    response_type, response_payload = await client.create_party_registration()
+    assert response_type == 'oadrCreatedPartyRegistration'
+    assert response_payload['ven_id'] == VEN_ID
+
+

+ 54 - 0
test/key.pem

@@ -0,0 +1,54 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIJnDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIx0KwpLX3E6ECAggA
+MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECJ//ErIFZQJJBIIJSKJgQttwaFke
+9rBpQLmpQgWVYt9XTcOmJNXnOAZdrRv0JzOoo2LYR+ejDSWFJyXznL3TLr4vywkn
+YvPM1QFIlt7EKWb5IeEe7zZa9aWAHa2rFq74a0mltzkggn99WWodB/IBDKlBOnHp
+dyw6qGDzwrTREvzdVwDF8hqRUNp6AccO9BGWgxr6ipdbvDUdHqUSb6HMle0hrQ7T
+P6wtaRr2OaRVJEwVGwFCW/lymMx+kXf7u5GnZyVfB/eMfe/iDu3hfj55m8RVWHZR
+LGSaTT7kkpsCjLPGzQwbz7qwxdGJ27K6Cil0J2eqpihcolTef+qNqUOWvmtkNS9r
+VxtFp4CtFG28Vz3OtqlP4QeZ2JDFUeHbvtCSn/Q8shlAo1PfxgDIf4vMXI1LVHok
+D8Cd//CczX3gIt/6V9eLDgb6nX6ozPnCond4o5aMp5VIt1/YQvYBROvhvCeCdAUh
+imfWmAwb7zJltFizuMek9kwpTnYt+3ZsA6gSTHTsS301U4ne0h1YEyL3SO9/gWeu
+m2vTEmefDY1vHp2EaHxFcWA3q3yyCoLQ0BvOBcgStnXzoM3Qo9d73obTizt950Vx
+jhLfBYRxoXTyJbIKajDU/apkSyT8BeiWuahkv+zvHt7fnTLwaJ42L+4WpCFRpQv+
+pjUiw8gX7ZCqtq3FC9xYQyHKV/6NDIc1evmtIXHBtS3uiMCm2Hxy6YOlQeOtGv4g
+QiloWHPi4fWz/nQ6RxUNZh3F1eKtTxPxInqKjRYO8T553r7mr45+xd5ldI3GZkND
+3eDIDeRdSqotLrbQrNZ9690J7VCpXnlwjvg4rsa4jQaCJG4Kyj7AdIUKlVJ+m2Pg
+f6UkYjJDpnl0eJ94HOa9fIHYnvXvzYc8YhEbfAGGvHMLV+L8fgE/t2rcu0WBZlUt
+QprcN8KNuaVrYJHxF4UYE6tcdu+4LzqJpZRY0JKmsryBi/URO9R5fpzOqMm/Hnk+
+OWo0mUqfJvIIKPpa7JNhk8P7vwUS5SLB+9kHJkYBRPQcnzUh3/ii6hR//8ogkW9u
+KaRd9QMANzO6f0MsgvxjqbqwNFVFCNDA6ZMF8wCeZDjl5ompxvD2ED50SUgwjKLf
+iInnHbe7TD02oBD0EzfR22d0+36Q+gsKV5xW1O+qhx159uvkgAVsxzqD1fa6ylBV
+ZNmmxfH8RwRiFimpvTluknwVQxUIGEsnXy3ubTS4W4J1alj5GQTvM2lGRha4HvHZ
+pT3y+1rSxcBHEA4iiKoAlRu+dUYjDaD5+39rvDLXev234w2dzise+H2ppaNIewLE
+UlOAs80zdLioiM6IVLnw2m1mJGnYbkkP+o/PKEjumnTvnMMbnl6CEXLWIucT8Okf
+/Jbe8NAS9lu4+1jnpImYewjNRuJbQsMHariIcjUNHW/8wUBA/Y7wOHExTw/vvY82
+xm6ZvfsN59U4UZPkMZpCrCqNh3USwhT4671+LlBNHiRFeddbm/Ko+6EgRiK1JCUB
+SlneKQybezlsWn+/hu3Ub+j+2qzBjcUnRSjLsv9KZem7Yz4FLAISwXK15iq2Z+7M
+FwKSZ12QjXjTPzmBXGRQaIQrccIWmFmgJ5mQ2T+oH0ucWFWO49vLvuh4oJ0H4kop
+xGNgqC/8HD61REtbADdQNwGwzdn5brVzohQ+8tTPWjsqGEqwyofIp/L7pavENJKg
+ho6RqWqa2CNA/dppwKQH6bO+X8CUQissOyCBovYqvKMwuRAsYwD2J5T64Zso+WTm
+3njYGfOS8hoyfjrW7qX3Pcxm5fBntxbeKbdgKzeCxNgVxdkiYpQ+h1CxFWArqvRo
+myAstRzSFNGGuC81q0Gm9F71mnD93Zmc1F/3np9A/WdzJ6YL0iti2lVwseCFMQjU
+8bQ/KxoBw4xWp1isy5xFilca7psVSwV+hnFaOxKf1CwL7ZEQpJaGcUtl5p16AGwW
+kezn+pDfK3SIvsZd3OS7mfrgbkeXcxQi6Dcfvdtvg+OvFGOdRwurKkPWIvegVWeb
+nkssQNTiQsEmxjnFdyPqNDB0ilL9viEwaPEchYZmKWVhnn6OwjvW0hwgx4YIb23k
+5P7jwlcyZLFwuhm4wahd7Veh9fEYQlaKpufXaMtUNkX+x4ts6Cy9Ap3qhjVp1JxK
+Vj5yjCCQBLsGUZ0CDjI0Oph3z54BrFAbbqVdedvypmEY1SXDYZ0d4RdcuUohkPDZ
+zJ74gZslR5qV6JkXGjZZUCvw4Ea49ZxCR5XhBrIqfuhfRrdPd4Quoh8LCUPlRa+q
+kAhjKVXliVOLL/yCM5jwqrnHhxGp5jBa32lQAfZHdkVTlG7xAjlCyTJg+n90gejN
+4m2hanUPC3C4qv4Fj0sdWrty9mPL5sKUmFT/RMlfEG8Y9DZXwx2nXWPMYsScO3qP
+Q/+u2G2Spj4ijuWZpfPmikqXIWQYHUEpeLiX38eEDnn4kGkT13aT4KzwUnISVQPi
+f+yIlqGFWPlS7ZwUh0jsq6UEzJ6CyjHwkxMRlLH8VseJOZlmlJqIq9K+59gZWaO2
+fFhqQHgrJIJEMIf+5e37n1SWUsfWkTDkyMtPa43QYV6d8Vh/c+mjRiApxDu3EZpg
+kgGc7uLESaht0FlUWs1p8Dn07WqSxc3qwzK3nv+XgLSR0XFbU+lar5SUp4PdjOKW
+QFmoKo77hjRFgVFh5FW+ZY1T4827yz2LgplQ1IMkx+fWT+rPEQOZfMOWzKkdc3DF
+55iTW0UglsKbS9j79JaXcIGOo7Wx1AvMFUijbbYei4PV2+GjWahKFXE/cClrlG8S
++oM+V1Y0SCX39CT6wRKMgeOCmtw/9wPFtoxPSAcet2V6ZbzkwEskAl7/xp+WpX6e
+TUpspg7bqhnUHsIBh5NI65INdYZbAA55ykuqMT01ZgxkFRyhymFBf32nE2dQ1UxX
+n95musHsifY8URg+0o9kGvlGzV7Yx98F9rojvd9oLnrmMjRpYEcrc42XwbNlVwx6
+Z7N4qIz8rKxYWWrAhg9BY4NLKQzmsMeVtKXgUVni2Vjv7cKqiAmAw1gBVhIvJLYe
+GVJwuZCOosmF31DNVQOU4XOTWNhjTpg0/jCbLtnrmiVcb70pGrKv4a3v3LyS59i6
+ohm0oZui3W+utcO4fPOlWhctui8OquUxrm2dWbfOPIa8yndNSvLWRKs6GAukaK+9
+J/RQZbipAoMuY1b7r9U/hQ==
+-----END ENCRYPTED PRIVATE KEY-----

+ 116 - 110
test/test_message_conversion.py

@@ -23,6 +23,24 @@ from datetime import datetime, timezone, timedelta
 
 DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 
+def test_messages():
+    for message_type, data in testcases:
+        _test_message(message_type, **data)
+
+def _test_message(message_type, **data):
+    message = create_message(message_type, **data)
+    # print(message)
+    parsed = parse_message(message)[1]
+
+    if parsed == data:
+        print(colored(f"pass {message_type}", "green"))
+    else:
+        pprint(data)
+        print(message)
+        pprint(parsed)
+        print(colored(f"fail {message_type}", "red"))
+        quit(1)
+
 def create_dummy_event(ven_id):
     """
     Creates a dummy event
@@ -63,34 +81,21 @@ def create_dummy_event(ven_id):
              'response_required': 'always'}
     return event
 
-def test_message(message_type, **data):
-    message = create_message(message_type, **data)
-    # print(message)
-    parsed = parse_message(message)[1]
-
-    if parsed == data:
-        print(colored(f"pass {message_type}", "green"))
-    else:
-        pprint(data)
-        print(message)
-        pprint(parsed)
-        print(colored(f"fail {message_type}", "red"))
-        quit(1)
-
-test_message('oadrCanceledOpt', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, opt_id=generate_id())
-test_message('oadrCanceledPartyRegistration', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, registration_id=generate_id(), ven_id='123ABC')
-test_message('oadrCanceledReport', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}])
-test_message('oadrCanceledReport', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}], ven_id='123ABC')
-test_message('oadrCancelOpt', request_id=generate_id(), ven_id='123ABC', opt_id=generate_id())
-test_message('oadrCancelPartyRegistration', request_id=generate_id(), ven_id='123ABC', registration_id=generate_id())
-test_message('oadrCancelReport', request_id=generate_id(), ven_id='123ABC', report_request_id=generate_id(), report_to_follow=True)
-test_message('oadrCreatedEvent', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
+testcases = [
+('oadrCanceledOpt', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, opt_id=generate_id())),
+('oadrCanceledPartyRegistration', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, registration_id=generate_id(), ven_id='123ABC')),
+('oadrCanceledReport', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}])),
+('oadrCanceledReport', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}], ven_id='123ABC')),
+('oadrCancelOpt', dict(request_id=generate_id(), ven_id='123ABC', opt_id=generate_id())),
+('oadrCancelPartyRegistration', dict(request_id=generate_id(), ven_id='123ABC', registration_id=generate_id())),
+('oadrCancelReport', dict(request_id=generate_id(), ven_id='123ABC', report_request_id=generate_id(), report_to_follow=True)),
+('oadrCreatedEvent', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
                                  event_responses=[{'response_code': 200, 'response_description': 'OK', 'request_id': generate_id(), 'event_id': generate_id(), 'modification_number': 1, 'opt_type': 'optIn'},
                                                   {'response_code': 200, 'response_description': 'OK', 'request_id': generate_id(), 'event_id': generate_id(), 'modification_number': 1, 'opt_type': 'optIn'},
                                                   {'response_code': 200, 'response_description': 'OK', 'request_id': generate_id(), 'event_id': generate_id(), 'modification_number': 1, 'opt_type': 'optIn'}],
-                                 ven_id='123ABC')
-test_message('oadrCreatedReport', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}], ven_id='123ABC')
-test_message('oadrCreatedEvent', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
+                                 ven_id='123ABC')),
+('oadrCreatedReport', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}], ven_id='123ABC')),
+('oadrCreatedEvent', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
                                  event_responses=[{'response_code': 200, 'response_description': 'OK', 'request_id': generate_id(),
                                                     'event_id': generate_id(),
                                                     'modification_number': 1,
@@ -99,15 +104,15 @@ test_message('oadrCreatedEvent', response={'response_code': 200, 'response_descr
                                                     'event_id': generate_id(),
                                                     'modification_number': 1,
                                                     'opt_type': 'optOut'}],
-                                 ven_id='123ABC')
-test_message('oadrCreatedPartyRegistration', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
+                                 ven_id='123ABC')),
+('oadrCreatedPartyRegistration', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
                                              registration_id=generate_id(),
                                              ven_id='123ABC',
                                              profiles=[{'profile_name': '2.0b',
                                                         'transports': [{'transport_name': 'simpleHttp'}]}],
-                                             vtn_id='VTN123')
-test_message('oadrCreatedReport', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}])
-test_message('oadrCreateOpt', opt_id=generate_id(),
+                                             vtn_id='VTN123')),
+('oadrCreatedReport', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, pending_reports=[{'request_id': generate_id()}, {'request_id': generate_id()}])),
+('oadrCreateOpt', dict(opt_id=generate_id(),
                               opt_type='optIn',
                               opt_reason='participating',
                               created_date_time=datetime.now(timezone.utc),
@@ -115,9 +120,9 @@ test_message('oadrCreateOpt', opt_id=generate_id(),
                               event_id=generate_id(),
                               modification_number=1,
                               targets=[{'ven_id': '123ABC'}],
-                              ven_id='VEN123')
-test_message('oadrCreatePartyRegistration', request_id=generate_id(), ven_id='123ABC', profile_name='2.0b', transport_name='simpleHttp', transport_address='http://localhost', report_only=False, xml_signature=False, ven_name='test', http_pull_model=True)
-test_message('oadrCreateReport', request_id=generate_id(),
+                              ven_id='VEN123')),
+('oadrCreatePartyRegistration', dict(request_id=generate_id(), ven_id='123ABC', profile_name='2.0b', transport_name='simpleHttp', transport_address='http://localhost', report_only=False, xml_signature=False, ven_name='test', http_pull_model=True)),
+('oadrCreateReport', dict(request_id=generate_id(),
                                  ven_id='123ABC',
                                  report_requests=[{'report_request_id': 'd2b7bade5f',
                                                   'report_specifier': {'granularity': timedelta(seconds=900),
@@ -127,11 +132,11 @@ test_message('oadrCreateReport', request_id=generate_id(),
                                                                                            'tolerance': {'tolerate': {'startafter': timedelta(seconds=300)}}},
                                                                        'report_specifier_id': '9c8bdc00e7',
                                                                        'specifier_payload': {'r_id': 'd6e2e07485',
-                                                                                             'reading_type': 'Direct Read'}}}])
-test_message('oadrDistributeEvent', request_id=generate_id(), response={'request_id': 123, 'response_code': 200, 'response_description': 'OK'}, events=[create_dummy_event(ven_id='123ABC')], vtn_id='VTN123')
-test_message('oadrPoll', ven_id='123ABC')
-test_message('oadrQueryRegistration', request_id=generate_id())
-test_message('oadrRegisteredReport', ven_id='VEN123', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
+                                                                                             'reading_type': 'Direct Read'}}}])),
+('oadrDistributeEvent', dict(request_id=generate_id(), response={'request_id': 123, 'response_code': 200, 'response_description': 'OK'}, events=[create_dummy_event(ven_id='123ABC')], vtn_id='VTN123')),
+('oadrPoll', dict(ven_id='123ABC')),
+('oadrQueryRegistration', dict(request_id=generate_id())),
+('oadrRegisteredReport', dict(ven_id='VEN123', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()},
                                      report_requests=[{'report_request_id': generate_id(),
                                                        'report_specifier': {'report_specifier_id': generate_id(),
                                                                             'granularity': timedelta(minutes=15),
@@ -155,10 +160,10 @@ test_message('oadrRegisteredReport', ven_id='VEN123', response={'response_code':
                                                                                                 'ramp_up': timedelta(minutes=15),
                                                                                                 'recovery': timedelta(minutes=5)},
                                                                             'specifier_payload': {'r_id': generate_id(),
-                                                                                                  'reading_type': 'Direct Read'}}}])
-test_message('oadrRequestEvent', request_id=generate_id(), ven_id='123ABC')
-test_message('oadrRequestReregistration', ven_id='123ABC')
-test_message('oadrRegisterReport', request_id=generate_id(), reports=[{'report_id': generate_id(),
+                                                                                                  'reading_type': 'Direct Read'}}}])),
+('oadrRequestEvent', dict(request_id=generate_id(), ven_id='123ABC')),
+('oadrRequestReregistration', dict(ven_id='123ABC')),
+('oadrRegisterReport', dict(request_id=generate_id(), reports=[{'report_id': generate_id(),
                                                                        'report_descriptions': {
                                                                             generate_id(): {
                                                                             'report_subjects': [{'ven_id': '123ABC'}],
@@ -172,72 +177,72 @@ test_message('oadrRegisterReport', request_id=generate_id(), reports=[{'report_i
                                                                        'report_name': 'HISTORY_USAGE',
                                                                        'created_date_time': datetime.now(timezone.utc)}],
                                                         ven_id='123ABC',
-                                                        report_request_id=generate_id())
-test_message('oadrRegisterReport', **{'request_id': '8a4f859883', 'reports': [{'report_id': generate_id(),
-                                                                               'duration': timedelta(seconds=7200),
-                                                                               'report_descriptions': {'resource1_status': {
-                                                                                                        'report_data_sources': [{'resource_id': 'resource1'}],
-                                                                                                        'report_type': 'x-resourceStatus',
-                                                                                                        'reading_type': 'x-notApplicable',
-                                                                                                        'market_context': 'http://MarketContext1',
-                                                                                                        'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
-                                                                                'report_request_id': '0',
-                                                                                'report_specifier_id': '789ed6cd4e_telemetry_status',
-                                                                                'report_name': 'METADATA_TELEMETRY_STATUS',
-                                                                                'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
-                                                                               {'report_id': generate_id(),
-                                                                                'duration': timedelta(seconds=7200),
-                                                                                'report_descriptions': {'resource1_energy': {
-                                                                                                         'report_data_sources': [{'resource_id': 'resource1'}],
-                                                                                                         'report_type': 'usage',
-                                                                                                         'energy_real': {'item_description': 'RealEnergy',
-                                                                                                                         'item_units': 'Wh',
-                                                                                                                         'si_scale_code': 'n'},
-                                                                                                         'reading_type': 'Direct Read',
-                                                                                                         'market_context': 'http://MarketContext1',
-                                                                                                         'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
-                                                                                                        'resource1_power': {
-                                                                                                         'report_data_sources': [{'resource_id': 'resource1'}],
-                                                                                                         'report_type': 'usage',
-                                                                                                         'power_real': {'item_description': 'RealPower',
-                                                                                                                        'item_units': 'W',
-                                                                                                                        'si_scale_code': 'n',
-                                                                                                                        'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
-                                                                                                          'reading_type': 'Direct Read',
-                                                                                                          'market_context': 'http://MarketContext1',
-                                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
-                                                                                'report_request_id': '0',
-                                                                                'report_specifier_id': '789ed6cd4e_telemetry_usage',
-                                                                                'report_name': 'METADATA_TELEMETRY_USAGE',
-                                                                                'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
-                                                                               {'report_id': generate_id(),
-                                                                                'duration': timedelta(seconds=7200),
-                                                                                'report_descriptions': {'resource1_energy': {
-                                                                                                         'report_data_sources': [{'resource_id': 'resource1'}],
-                                                                                                         'report_type': 'usage',
-                                                                                                         'energy_real': {'item_description': 'RealEnergy',
-                                                                                                                         'item_units': 'Wh',
-                                                                                                                         'si_scale_code': 'n'},
-                                                                                                         'reading_type': 'Direct Read',
-                                                                                                         'market_context': 'http://MarketContext1',
-                                                                                                         'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
-                                                                                                        'resource1_power': {
-                                                                                                         'report_data_sources': [{'resource_id': 'resource1'}],
-                                                                                                         'report_type': 'usage',
-                                                                                                         'power_real': {'item_description': 'RealPower',
-                                                                                                                        'item_units': 'W', 'si_scale_code': 'n',
-                                                                                                                        'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
-                                                                                                         'reading_type': 'Direct Read',
-                                                                                                         'market_context': 'http://MarketContext1',
-                                                                                                         'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
-                                                                                'report_request_id': '0',
-                                                                                'report_specifier_id': '789ed6cd4e_history_usage',
-                                                                                'report_name': 'METADATA_HISTORY_USAGE',
-                                                                                'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)}], 'ven_id': 's3cc244ee6'})
-test_message('oadrResponse', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, ven_id='123ABC')
-test_message('oadrResponse', response={'response_code': 200, 'response_description': 'OK', 'request_id': None}, ven_id='123ABC')
-test_message('oadrUpdatedReport', response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, ven_id='123ABC', cancel_report={'request_id': generate_id(), 'report_request_id': [generate_id(), generate_id(), generate_id()], 'report_to_follow': False, 'ven_id': '123ABC'})
-test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id': generate_id(),
+                                                        report_request_id=generate_id())),
+('oadrRegisterReport', {'request_id': '8a4f859883', 'reports': [{'report_id': generate_id(),
+                                                                 'duration': timedelta(seconds=7200),
+                                                                 'report_descriptions': {'resource1_status': {
+                                                                                          'report_data_sources': [{'resource_id': 'resource1'}],
+                                                                                          'report_type': 'x-resourceStatus',
+                                                                                          'reading_type': 'x-notApplicable',
+                                                                                          'market_context': 'http://MarketContext1',
+                                                                                          'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
+                                                                  'report_request_id': '0',
+                                                                  'report_specifier_id': '789ed6cd4e_telemetry_status',
+                                                                  'report_name': 'METADATA_TELEMETRY_STATUS',
+                                                                  'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
+                                                                 {'report_id': generate_id(),
+                                                                  'duration': timedelta(seconds=7200),
+                                                                  'report_descriptions': {'resource1_energy': {
+                                                                                           'report_data_sources': [{'resource_id': 'resource1'}],
+                                                                                           'report_type': 'usage',
+                                                                                           'energy_real': {'item_description': 'RealEnergy',
+                                                                                                           'item_units': 'Wh',
+                                                                                                           'si_scale_code': 'n'},
+                                                                                           'reading_type': 'Direct Read',
+                                                                                           'market_context': 'http://MarketContext1',
+                                                                                           'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
+                                                                                          'resource1_power': {
+                                                                                           'report_data_sources': [{'resource_id': 'resource1'}],
+                                                                                           'report_type': 'usage',
+                                                                                           'power_real': {'item_description': 'RealPower',
+                                                                                                          'item_units': 'W',
+                                                                                                          'si_scale_code': 'n',
+                                                                                                          'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
+                                                                                            'reading_type': 'Direct Read',
+                                                                                            'market_context': 'http://MarketContext1',
+                                                                                            'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
+                                                                  'report_request_id': '0',
+                                                                  'report_specifier_id': '789ed6cd4e_telemetry_usage',
+                                                                  'report_name': 'METADATA_TELEMETRY_USAGE',
+                                                                  'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)},
+                                                                 {'report_id': generate_id(),
+                                                                  'duration': timedelta(seconds=7200),
+                                                                  'report_descriptions': {'resource1_energy': {
+                                                                                           'report_data_sources': [{'resource_id': 'resource1'}],
+                                                                                           'report_type': 'usage',
+                                                                                           'energy_real': {'item_description': 'RealEnergy',
+                                                                                                           'item_units': 'Wh',
+                                                                                                           'si_scale_code': 'n'},
+                                                                                           'reading_type': 'Direct Read',
+                                                                                           'market_context': 'http://MarketContext1',
+                                                                                           'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}},
+                                                                                          'resource1_power': {
+                                                                                           'report_data_sources': [{'resource_id': 'resource1'}],
+                                                                                           'report_type': 'usage',
+                                                                                           'power_real': {'item_description': 'RealPower',
+                                                                                                          'item_units': 'W', 'si_scale_code': 'n',
+                                                                                                          'power_attributes': {'hertz': 60, 'voltage': 110, 'ac': False}},
+                                                                                           'reading_type': 'Direct Read',
+                                                                                           'market_context': 'http://MarketContext1',
+                                                                                           'sampling_rate': {'min_period': timedelta(seconds=60), 'max_period': timedelta(seconds=60), 'on_change': False}}},
+                                                                  'report_request_id': '0',
+                                                                  'report_specifier_id': '789ed6cd4e_history_usage',
+                                                                  'report_name': 'METADATA_HISTORY_USAGE',
+                                                                  'created_date_time': datetime(2019, 11, 20, 15, 4, 52, 638621, tzinfo=timezone.utc)}], 'ven_id': 's3cc244ee6'}),
+('oadrResponse', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, ven_id='123ABC')),
+('oadrResponse', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': None}, ven_id='123ABC')),
+('oadrUpdatedReport', dict(response={'response_code': 200, 'response_description': 'OK', 'request_id': generate_id()}, ven_id='123ABC', cancel_report={'request_id': generate_id(), 'report_request_id': [generate_id(), generate_id(), generate_id()], 'report_to_follow': False, 'ven_id': '123ABC'})),
+('oadrUpdateReport', dict(request_id=generate_id(), reports=[{'report_id': generate_id(),
                                                                                   'report_name': enums.REPORT_NAME.values[0],
                                                                                   'created_date_time': datetime.now(timezone.utc),
                                                                                   'report_request_id': generate_id(),
@@ -249,11 +254,11 @@ test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id'
                                                                                                                           'market_context': 'http://localhost',
                                                                                                                           'sampling_rate': {'min_period': timedelta(minutes=1),
                                                                                                                                             'max_period': timedelta(minutes=2),
-                                                                                                                                            'on_change': False}}}}], ven_id='123ABC')
+                                                                                                                                            'on_change': False}}}}], ven_id='123ABC'))
 # for report_name in enums.REPORT_NAME.values:
 #     for reading_type in enums.READING_TYPE.values:
 #         for report_type in enums.REPORT_TYPE.values:
-#             test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id': generate_id(),
+#             ('oadrUpdateReport', dict(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(),
@@ -268,3 +273,4 @@ test_message('oadrUpdateReport', request_id=generate_id(), reports=[{'report_id'
 #                                                                                                                              'max_period': timedelta(minutes=2),
 #                                                                                                                              'on_change': False}}]}], ven_id='123ABC')
 
+]

File diff suppressed because it is too large
+ 39 - 35
test/test_schema.py


+ 21 - 11
test/test_signatures.py

@@ -16,19 +16,22 @@
 
 
 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 pyopenadr.messaging import create_message, parse_message
 from hashlib import sha256
 from base64 import b64encode
 from datetime import datetime, timedelta, timezone
+import os
+
+with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'cert.pem'), 'rb') as file:
+    TEST_CERT = file.read()
+with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'key.pem'), 'rb') as file:
+    TEST_KEY = file.read()
+TEST_KEY_PASSWORD = 'openadr'
 
 def test_message_validation():
-    msg = create_message('oadrPoll', ven_id='123')
-    parsed_type, parsed_message = parse_message(msg)
+    msg = create_message('oadrPoll', ven_id='123', cert=TEST_CERT, key=TEST_KEY, passphrase='openadr')
+    parsed_type, parsed_message = parse_message(msg, cert=TEST_CERT)
     assert parsed_type == 'oadrPoll'
-    validate_message(msg)
-
 
 
 def test_message_validation_complex():
@@ -73,7 +76,14 @@ def test_message_validation_complex():
     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)
+                         events=[event],
+                         cert=TEST_CERT,
+                         key=TEST_KEY,
+                         passphrase='openadr')
+    parsed_type, parsed_msg = parse_message(msg, cert=TEST_CERT)
+
+if __name__ == "__main__":
+    msg = create_message('oadrPoll', ven_id='123', signing_certificate=TEST_CERT, signing_key=TEST_KEY, signing_key_passphrase=b'openadr')
+    parsed_type, parsed_message = parse_message(msg)
+    validate_message(msg, public_key=TEST_CERT)
+    print(msg)

+ 0 - 2
test_requirements.txt

@@ -1,2 +0,0 @@
-lxml
-termcolor

Some files were not shown because too many files changed in this diff