├── .github └── workflows │ └── test-suite.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── VERSION ├── certificates ├── dummy_ca.crt ├── dummy_ca.key ├── dummy_ca.srl ├── dummy_ven.crt ├── dummy_ven.csr ├── dummy_ven.key ├── dummy_vtn.crt ├── dummy_vtn.csr ├── dummy_vtn.key └── generate_certificates.sh ├── dev_requirements.txt ├── docs ├── Makefile ├── _static │ ├── css │ │ └── custom.css │ └── logo-tall.png ├── api │ ├── modules.rst │ └── openleadr.rst ├── client.rst ├── conf.py ├── features.rst ├── index.rst ├── logging.rst ├── make.bat ├── message_signing.rst ├── reporting.rst ├── representations.rst ├── roadmap.rst └── server.rst ├── logo.png ├── openleadr ├── __init__.py ├── client.py ├── enums.py ├── errors.py ├── fingerprint.py ├── hooks.py ├── messaging.py ├── objects.py ├── preflight.py ├── schema │ ├── LICENSES.txt │ ├── oadr_20b.xsd │ ├── oadr_ISO_ISO3AlphaCurrencyCode_20100407.xsd │ ├── oadr_atom.xsd │ ├── oadr_ei_20b.xsd │ ├── oadr_emix_20b.xsd │ ├── oadr_gml_20b.xsd │ ├── oadr_greenbutton.xsd │ ├── oadr_power_20b.xsd │ ├── oadr_pyld_20b.xsd │ ├── oadr_siscale_20b.xsd │ ├── oadr_strm_20b.xsd │ ├── oadr_xcal_20b.xsd │ ├── oadr_xml.xsd │ ├── oadr_xmldsig-properties-schema.xsd │ ├── oadr_xmldsig.xsd │ └── oadr_xmldsig11.xsd ├── server.py ├── service │ ├── __init__.py │ ├── decorators.py │ ├── event_service.py │ ├── opt_service.py │ ├── poll_service.py │ ├── registration_service.py │ ├── report_service.py │ └── vtn_service.py ├── templates │ ├── oadrCancelOpt.xml │ ├── oadrCancelPartyRegistration.xml │ ├── oadrCancelReport.xml │ ├── oadrCanceledOpt.xml │ ├── oadrCanceledPartyRegistration.xml │ ├── oadrCanceledReport.xml │ ├── oadrCreateOpt.xml │ ├── oadrCreatePartyRegistration.xml │ ├── oadrCreateReport.xml │ ├── oadrCreatedEvent.xml │ ├── oadrCreatedOpt.xml │ ├── oadrCreatedPartyRegistration.xml │ ├── oadrCreatedReport.xml │ ├── oadrDistributeEvent.xml │ ├── oadrPayload.xml │ ├── oadrPoll.xml │ ├── oadrQueryRegistration.xml │ ├── oadrRegisterReport.xml │ ├── oadrRegisteredReport.xml │ ├── oadrRequestEvent.xml │ ├── oadrRequestReregistration.xml │ ├── oadrResponse.xml │ ├── oadrUpdateReport.xml │ ├── oadrUpdatedReport.xml │ └── parts │ │ ├── eiActivePeriod.xml │ │ ├── eiEvent.xml │ │ ├── eiEventDescriptor.xml │ │ ├── eiEventSignal.xml │ │ ├── eiEventTarget.xml │ │ ├── eiTarget.xml │ │ ├── emixInterface.xml │ │ ├── eventSignalEmix.xml │ │ ├── oadrReportDescription.xml │ │ ├── oadrReportRequest.xml │ │ └── reportDescriptionEmix.xml └── utils.py ├── pytest.ini ├── setup.py └── test ├── __init__.py ├── conformance ├── test_conformance_001.py ├── test_conformance_002.py ├── test_conformance_006.py ├── test_conformance_008.py ├── test_conformance_009.py ├── test_conformance_014.py └── test_conformance_021.py ├── fixtures ├── __init__.py └── simple_server.py ├── integration_tests ├── test_client_registration.py └── test_event_warnings_errors.py ├── test_certificates.py ├── test_client_misc.py ├── test_errors.py ├── test_event_distribution.py ├── test_failures.py ├── test_message_conversion.py ├── test_objects.py ├── test_poll_responses.py ├── test_registrations.py ├── test_reports.py ├── test_schema.py ├── test_signatures.py └── test_utils.py /.github/workflows/test-suite.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Test Suite 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.7, 3.8, 3.9] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install . 30 | pip install flake8 pytest 31 | if [ -f dev_requirements.txt ]; then pip install -r dev_requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 openleadr/ --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 openleadr/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pytest -vv test/ 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | python_env/ 2 | __pycache__ 3 | pyopenadr.egg-info 4 | docs/_build/ 5 | build/ 6 | openleadr.egg-info/ 7 | htmlcov/ 8 | examples/ 9 | dist/ 10 | .pytest_cache/ 11 | .ipynb_checkpoints/ 12 | static/ 13 | .idea/ 14 | .vscode/ 15 | *.venv/ 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at team@openleadr.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include openleadr/templates/*.xml 2 | include openleadr/templates/parts/*.xml 3 | include openleadr/schema/*.xsd 4 | include openleadr/schema/LICENSES.txt 5 | exclude test/* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Test Suite](https://github.com/OpenLEADR/openleadr-python/workflows/Test%20Suite/badge.svg) 2 | [![Test Coverage](https://openleadr.org/coverage/badge.svg)](https://openleadr.org/coverage) 3 | ![PyPI Downloads](https://img.shields.io/pypi/dm/openleadr?color=lightblue&label=PyPI%20Downloads) 4 | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4317/badge)](https://bestpractices.coreinfrastructure.org/projects/4317) 5 | 6 | ![OpenLEADR](https://openleadr.org/images/lf-logo.png) 7 | 8 | OpenLEADR is a Python 3 module that provides a convenient interface to OpenADR 9 | systems. It contains an OpenADR Client that you can use to talk to other OpenADR 10 | systems, and it contains an OpenADR Server (VTN) with convenient integration 11 | possibilities. 12 | 13 | It currently implements the OpenADR 2.0b specification. 14 | 15 | ## Documentation 16 | 17 | You can find documentation here: https://openleadr.org/docs. 18 | 19 | 20 | ## Getting started 21 | 22 | We have cool 1-minute starting examples for both [VEN](https://openleadr.org/docs/client.html#example-ven) and [VTN](https://openleadr.org/docs/server.html#minute-vtn-example). Please check them out and take a look at the rest of the documentation. 23 | 24 | 25 | ## Feature Completeness 26 | 27 | At the moment, you can do basic Registration, Telemetry Reports and Events over a secured connection using signed messages. This covers many use-cases that allow for demand response to be coordinated. Below is a more extensive list of OpenADR features and their current implementation status in OpenLEADR: 28 | 29 | The following is an overview of OpenADR 2.0b features that are implemented in OpenADR: 30 | 31 | | Feature | Implemented | Remarks | 32 | |------------------------|-------------|--------------------------------------------------| 33 | | **Transport** | 34 | | HTTP Pull Method | ✔️ | Most-used method, allows VEN to be behind NAT | 35 | | HTTP Push Method | ❌ | | 36 | | XMPP Transport | ❌ | | 37 | | Message Signing | ✔️ | Official OpenADR Alliance Root Cert not included | 38 | | **Registration** | 39 | | Registration procedure | ✔️ | | 40 | | **Reporting** | 41 | | Telemetry Reports | ✔️ | | 42 | | Status Reports | ❌ | Work is in progress, see #108 | 43 | | History Reports | ❌ | | 44 | | **Events** | 45 | | Events | ✔️ | | 46 | | EiOpt | ❌ | | 47 | 48 | 49 | ## Contributing 50 | 51 | If you'd like to help make OpenLEADR better, you can do so in the following ways: 52 | 53 | 54 | ### File bug report or feature request 55 | 56 | We keep track of all bugs and feature requests on [Github Issues](https://github.com/openleadr/openleadr-python/issues). Please search the already close issues to see if your question was asked before. 57 | 58 | You're also very welcome to leave comments on existing issues with your ideas on how to improve OpenLEADR. 59 | 60 | 61 | ### File a pull request 62 | 63 | We'd love for you to contribute code to the project. We'll take a look at all pull requests, no matter their state. Please note though, that we will only accept pull requests if they meet at least the following criteria: 64 | 65 | - Your code style is flake8 compliant (with a maximum line length of 127 characters) 66 | - You provide tests for your new code and your code, and you amend any previous tests that fail if they are impacted by your code change 67 | - Your pull request refers to an Issue on our issue tracker, so that we and other people can see what problem is being solved. 68 | - You sign off your commits (`git commit -s`), to indicate that your contribution complies with our license and does not violate anybody else's copyright. 69 | 70 | That said, please don't let the above requirements discourage you from filing a pull requests. If you don't meet all of the above requirements, we'll help you fix the remaining things to get it into shape. 71 | 72 | 73 | ### Security issues 74 | 75 | Let it be clear that this code base is still in a development stage, and we don't yet recommend using it for mission critical applications or applications where security is paramount. 76 | 77 | That said, we do try to make OpenLEADR as secure as can be to work with. If you find a security vulnerability in OpenLEADR, please let us know at security AT openleadr DOT org. We will get back to you within 72 hours to follow up. We are committed to the following steps: 78 | 79 | - Security vulnerabilities with a known fix will be addressed as soon as possible. This means that work on other things will be put on hold until the security issue is fixed. 80 | - If a fix is not readily available, we will publish a warning that describes the vulnerable situation and, if possible, any mitigating steps that users of OpenLEADR can take. 81 | - After any security issue is fixed, we will publish information on it in the Changelog. 82 | 83 | 84 | ## Developing 85 | 86 | We recommend the following development setup for working with OpenLEADR (this is on Linux / macOS): 87 | 88 | ```bash 89 | git clone https://github.com/openleadr/openleadr-python 90 | cd openleadr-python 91 | python3 -m venv python_env 92 | ./python_env/bin/pip3 install -e . 93 | ./python_env/bin/pip3 install -r dev_requirements.txt 94 | ``` 95 | 96 | To run the test suite, you can use the following command: 97 | 98 | ```bash 99 | ./python_env/bin/python3 -m pytest -v test/ 100 | ``` 101 | 102 | ## Contributing Documentation 103 | 104 | ### Building the documentation 105 | 106 | Static documentation can be found in the docs directory. Edit or create new .rst files to add new content using 107 | the [Restructured Text](http://docutils.sourceforge.net/docs/user/rst/quickref.html) format. To see the results of your changes, 108 | the documentation can be built locally through the command line using the following instructions: 109 | 110 | ```bash 111 | # Ensure that you follow the development setup in the Developing section above before running the following commands 112 | # After running this these commands, you should see a new directory `docs/_build`, which contains the HTML documentation. 113 | # For more details, see https://www.sphinx-doc.org/en/master/tutorial/first-steps.html 114 | cd docs 115 | make html 116 | ``` 117 | 118 | Then, open your browser to the created local files: 119 | 120 | ``` 121 | file:///home//openleadr-python/docs/_build/html/index.html 122 | ``` 123 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.34 2 | -------------------------------------------------------------------------------- /certificates/dummy_ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFlzCCA3+gAwIBAgIUSQ322yCNFTGz4Jj2OTaoJdztPQYwDQYJKoZIhvcNAQEL 3 | BQAwWzELMAkGA1UEBhMCTkwxDjAMBgNVBAgMBU90aGVyMRswGQYDVQQKDBJPcGVu 4 | TEVBRFIgRHVtbXkgQ0ExHzAdBgNVBAMMFmR1bW15LWNhLm9wZW5sZWFkci5vcmcw 5 | HhcNMjIwNjE2MTgyNzM5WhcNMzIwNjEzMTgyNzM5WjBbMQswCQYDVQQGEwJOTDEO 6 | MAwGA1UECAwFT3RoZXIxGzAZBgNVBAoMEk9wZW5MRUFEUiBEdW1teSBDQTEfMB0G 7 | A1UEAwwWZHVtbXktY2Eub3BlbmxlYWRyLm9yZzCCAiIwDQYJKoZIhvcNAQEBBQAD 8 | ggIPADCCAgoCggIBAMqwnpqAyJ9qxDlGdkMdizQfRLXCPl/kz8p5oV978IH1DnB0 9 | LkufP341KcWv7XHJcy01YXEqeMNAJMeU7qtfTPhPDTszhXDOe1liEvUDA3jQjAdG 10 | a+cPn+FIj1fs7V7oX/B2Wif2Xs7innzQp8WnsNlxSPCecBEze7hhG2ptCJe5jMdE 11 | dSQpZDMdJpvoMo546ZFaEoxPqZWF3S+/oIoEv3nOyyVf73KZWEqC85wjvuibGLOs 12 | 4YZeXtIYgiKlrKn902C1m7PmJXz3oAk/sZTUUe1tFtHHtltcZifrwqIU2VNxq8/R 13 | JU3zWUumRF93Rll4KaiKFv6jogDWrqcfaq0nKsA+12UkgHfGIXRalHsdDvNRa6Gh 14 | H1Cs7nptpoiGpUxZras5E4onfaexSRfnrJsq+pH9y7TmRoxuTKPNhrJ0M0gFQqUE 15 | BUydgHsRwQYXtSf5HvDftE9JlWKfNsaPvFlEPw9vLZrcRL/U18ZF6RYmoYhaesug 16 | nriMVN3xziVixYNZQ/ndEm0ttF7XewHxvh092MbKC8Y/QEgqcDd7C3btHJ5NjYE/ 17 | 2AsZjc4yKpCxJfItEqTR4UXIvDMSB0jjTXhYlJQ0pOHJY+FwralkmUTuQVIqsBNE 18 | ssAMSVHgaiZAY/eiTLau1sOXdwSQVaLQTSxX1ygvaFJ1H/1TSVnwSt2siKu5AgMB 19 | AAGjUzBRMB0GA1UdDgQWBBRzKwgRWL08THbPeRqqW+LPOvZPPTAfBgNVHSMEGDAW 20 | gBRzKwgRWL08THbPeRqqW+LPOvZPPTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 21 | DQEBCwUAA4ICAQAdkKXAaQ7NR3RBhus/DGKxDVdRu+wgYvnmlYSPQ/OAmvLgyzbC 22 | a1/PJzYk/LUyXFOBc23XPG24P6HldOePrsSyuniQGbmjB2cYjATuiwjxpM+1JJ1g 23 | BbXzApaHvlXA6HtUKKNL6Vd8kjcAuNUpphr1VTJJuy3FlrwiSymkfzPT4u3x4Glb 24 | vjeI2Sfwp7z9qPrcofY3vLlqAuY4R3CntD9BCt0tmz6FuRpj0WcDDFPHXf9ukBvu 25 | H1Td12Gb+vug0k3Dx9Hg7bow5/Xxz2nlaXHkPSEGfiEVsU5y9t+xz5OgzfRIJZVg 26 | K6t2wG082D6Ydm6CEZ0jD9QXHb1qCAE6ROiA3LmxMizilcShCxFlmGOGzYcBn+Uk 27 | u10b9vF1/lBI0WmgalrwKh9fiVc3yFxMGmRO5HJUUmUArota34M8UW9fXbrBL2Md 28 | mWr4qS9YpdTpJgKWb/M6Msz1L6+IRKIwNGgp95mrMhtnPMbyfeN9+0Ej9dO6+vDA 29 | ic35lK91ZOKnMCou3ahhM2jzWRyJ3XAcTxLHdHVY8qRBZWIzVMYYCngNnsbGcdCT 30 | HtfF33gYY9RjMl5AkwlEBLVQ+vyCwjdRyGnvN+b+zVnW4dCladj976mUVRrlMhvp 31 | 5KWfj2VNLb6Rw5ljxApLcHATii/OJ6mDcNI/Iuzhz2OWiezbhtB3iHPx3g== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /certificates/dummy_ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJJwIBAAKCAgEAyrCemoDIn2rEOUZ2Qx2LNB9EtcI+X+TPynmhX3vwgfUOcHQu 3 | S58/fjUpxa/tcclzLTVhcSp4w0Akx5Tuq19M+E8NOzOFcM57WWIS9QMDeNCMB0Zr 4 | 5w+f4UiPV+ztXuhf8HZaJ/ZezuKefNCnxaew2XFI8J5wETN7uGEbam0Il7mMx0R1 5 | JClkMx0mm+gyjnjpkVoSjE+plYXdL7+gigS/ec7LJV/vcplYSoLznCO+6JsYs6zh 6 | hl5e0hiCIqWsqf3TYLWbs+YlfPegCT+xlNRR7W0W0ce2W1xmJ+vCohTZU3Grz9El 7 | TfNZS6ZEX3dGWXgpqIoW/qOiANaupx9qrScqwD7XZSSAd8YhdFqUex0O81FroaEf 8 | UKzuem2miIalTFmtqzkTiid9p7FJF+esmyr6kf3LtOZGjG5Mo82GsnQzSAVCpQQF 9 | TJ2AexHBBhe1J/ke8N+0T0mVYp82xo+8WUQ/D28tmtxEv9TXxkXpFiahiFp6y6Ce 10 | uIxU3fHOJWLFg1lD+d0SbS20Xtd7AfG+HT3YxsoLxj9ASCpwN3sLdu0cnk2NgT/Y 11 | CxmNzjIqkLEl8i0SpNHhRci8MxIHSONNeFiUlDSk4clj4XCtqWSZRO5BUiqwE0Sy 12 | wAxJUeBqJkBj96JMtq7Ww5d3BJBVotBNLFfXKC9oUnUf/VNJWfBK3ayIq7kCAwEA 13 | AQKCAgBdkKyWa+6w0ItmWSWMk93HoMuKD/HVOH3HXOBmgIMkqqgQt0ELvaaEryvq 14 | Su0UQsc9Tk/9+nomv7x6uUB3sZWJZoyYEI9/5IFCYKiZm9uFcNfDH/n9ftPyHhm1 15 | n/RvhFuNWEUD/5ICdNBuk69u6ZsUtVvTX4AiIJ7zHTiXp195Erlu3yYoHEdZ5RIG 16 | lGiKJjzD0U2QodGJ6XKScSY9sDVnmFNknGWfhDQiqVKleEbPf5EmmB6/dV7WI2Qn 17 | dE1BZ5+lHBCVOh+CEZk5y8JBDsYHEP2gt01x3TR3JzkAsxkjkOycEVLkKfrFlion 18 | 25qJAJRSbfRxdb36HyAem70V02fC5hirQ8j59/u0I8v2URd/NvDJ/f0B3qPzZb2m 19 | p59Sm5co1kd52Y4M7syvNwb0ZGIbRq4avAja025CUPBxy+IyAelLSLDHb4pGGlrs 20 | H9rQSmeHwQrdtoTmT+bupOWp3fa0KqEvFpAY8jF7XAiEHusgc+zRK+hoI4W/kwyb 21 | tZN5ME27K0d3hnHMv4ULWZqm85hnUbvxS/KFUHJfsdqmQcgnemK2nt7TGoGB2r4S 22 | kx8fm+5cWu1Y51au91wzjnewxj+oeX4m4c/c0l3gs7/2yRDvQPXkwd721WXQRGYm 23 | IO8hDdtdDQ5tIMzWLne3PxFHeX44sch4DZevJVCl/4k+xWbSFQKCAQEA9ZEbtibO 24 | hVvEa4bJXNpSq5lAS2bgev3CRJo94mi2W4cfhCaIWR1yCP9zubUKoCNf+1Y1iyW9 25 | vpsh5KMj3fYyxDZim9jdEqSX3kOaDR3c9n5mkOtyzZ6djQ9PIwDj/cAyR8w9MmAK 26 | JcuZ0YBHBXyyUFvMoOUgrzfZcs7X44HOupOyL/0+i1iVmNq28hvMVTGRb3873ynZ 27 | jJFZk3oIxaNN9YOTNbkDx8FSc56rHryhOjvh/cbdeBL+l0GYi3J88J1blt9bEs2j 28 | hmEdaFKsVSecb3e5ZoR2Jy1txN/38OyQObWn7trehvzP13YsNyuask6xk/sogL8o 29 | cHPqbIZFHF382wKCAQEA000p0l5ij+LNyIz4ShHP+MqYhGnjEb54zLo9rxfzvDeE 30 | TqkFwfRZZL9Y4yuV1AYjBEzrODNbcLf7poG6+cBfFXmCn/z624vZR0PtZsJhUG2Q 31 | YxaNfQzkDUcrPA5wLQurg1m8TfGtdvmT+V+Xu9XH/gJ8pQQmuCTaP0bvcwBkftAi 32 | o1IZ/wgCzwgL2ZBJj8ypbOaZ3JcTxOzabufrTZdx2UbfdoyT1hI0+7Ol/th7v6/B 33 | 5TC9dCDp9o1xXRDdp4LuPiVhWHDvpgWcCnx6Nfqxrc6ArS4N4S1Xq8Ds5g/a2veM 34 | 3RSk/Qg02LUsOk1FOn3gNDs22aHWsmqEFY6jy5CT+wKCAQBupwtgoc3vg7FXbm3v 35 | 3CTiU/UHxPykDxJpzULTcbGyPonyB1brKPyIl2szJCP8ktQeMfOAluoQKGE2YFi6 36 | HMU1avg2F5tOWkJgkf+pp2o43C4lYSLjCnUd6ecT88PIRMGjXqG4wFPyQfM21uGp 37 | 1E2ZLjRfz79RlQ3z0MaxKn3XztO9EhULv0fxj6ReJi6FuQc6wY9d+MKht6Ewdg3b 38 | 2ME5PyenTo4ohbE8jiO3fvH7hp5Ht0N2ZsC3sYQmWdl4pr9tlYm5SFZGKB6TMqWa 39 | 3XV9OOqfClMfRjYvP2i+9CntBzD7zT32f5a8FPDJj2lurU0RTFws33Y+bc0VR89+ 40 | f89/AoIBABm2bxtpXucRe2cnOGOTDVLCHJMoa3hSTFLlavIuoPtLai9ERC4Y9PPc 41 | kQ171UyqvoIcdhctsLfju5zEdqNDtI5hbfLYDxK+Tt9sNaxmhP8LhiF4shkAg5PW 42 | ED3Lp/zjSai/N7noaeprboVPC6DQ3/haBx7xhWCtI13F3QeKibAg1cY4DG/LYsQc 43 | v4xnEXyNNCTLXvfH7qDtDZunXyve/PyqsjCz5J2NM6NZdh6v60clNRwVtUg3ZSEa 44 | jyv7DG4A8crgM5tWiuMSFa30/c4pxotW8LOiQhAfu2ZsIKfSUBiLPTn99CHSaNPi 45 | IBKsKnt2q9zPQ9px5jVp3s7cv7Pa0RcCggEAPVZz9uR8BQIt7DFv6k6EfSkGYYIm 46 | Rsn6H1tLgmYM1eID5rHRCXGB7QEcJfKv6E3gosA9VuFl6FrRiWHlMlPYXUSMg/5M 47 | k85bmKxA0CcmnaiqHeu69+bKpoVobZNsEzUeTD2Fjl1nKy3y8Z3o+WVruxEgjt6D 48 | gDEt5Rd8BchSJnF/IaAKHQ7iXERCgoZXJMkOR8fcWDGQk6OfZZDQl+3lEyXciuhv 49 | +xgSs2GQn3rirmBkdvyOtCeG+fC5lp8bcPwUkw/1ux2lMwaB6iOv+zLr5L2Mp8mb 50 | NqXyPJ64Z8j4nUjnOGtIRNBA6jkbFg+uxQdBGrUq4vSL6nhvpkCaHx1siw== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /certificates/dummy_ca.srl: -------------------------------------------------------------------------------- 1 | 5032FCA390507718BC6EB5EEF9F072D1F4D5E942 2 | -------------------------------------------------------------------------------- /certificates/dummy_ven.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEPzCCAicCFFAy/KOQUHcYvG617vnwctH01elCMA0GCSqGSIb3DQEBCwUAMFsx 3 | CzAJBgNVBAYTAk5MMQ4wDAYDVQQIDAVPdGhlcjEbMBkGA1UECgwST3BlbkxFQURS 4 | IER1bW15IENBMR8wHQYDVQQDDBZkdW1teS1jYS5vcGVubGVhZHIub3JnMB4XDTIy 5 | MDYxNjE4MjczOVoXDTMyMDYxMzE4MjczOVowXTELMAkGA1UEBhMCTkwxDjAMBgNV 6 | BAgMBU90aGVyMRwwGgYDVQQKDBNPcGVuTEVBRFIgRHVtbXkgVkVOMSAwHgYDVQQD 7 | DBdkdW1teS12ZW4ub3BlbmxlYWRyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEP 8 | ADCCAQoCggEBAKg3/uwscG3+jkOWSrk4s6h4Hd1jIE7c73FH+2b0iRxb9pbBbU6L 9 | KnggNd3Sq+rAqUcL6/EHx7+vtBvvLSBu6xb5j2bDf1LboRet+Lg2Vof8cQavt7Bh 10 | J0XPEUrQ6GGv7FNfq8o0/+ao1IHS77LuWn7nNbjzdh0ZMhEoQpFh0KA9cTzUIUB0 11 | TTCSCMHsvOix8EVa0I/LK9KnlK8ebQU16wgK4Xvf3hjxqfkI5nza4EXsvY5pkJi5 12 | a3207Q1eORLiekOjgeU/tVoHgqcZF79tZiyk7wwA3LIfqMnsd54FJocWeXxN17OA 13 | fhVAgrq1t14OU7EH35H8rGLSRZW8+v1rRA8CAwEAATANBgkqhkiG9w0BAQsFAAOC 14 | AgEANnMeIW0wmij9MkjDHPOJoHbRT/yGuAndBhyP36M5BHvWbzM9MKoOtoAk3gCn 15 | kwm8XbRFoPe+/PMhPJPPGhBs0P33hP/N8wkIV/ev/sVcXjfD/9sndrs+ucT2nYV3 16 | 7FIxOrjIeJlBupAh0HqQRwryh1KGGmUuoCDHIDo0Vcjnl3H63MFs1MDGruWDoGjc 17 | e3mOkkfpEh2p6GRKHZMxXg4DxRPq4T0j8unaDXid3bqzvYgCoNxD7btxZ5iv/I6W 18 | mpwrFknvxX2KWcfVMVq6h7OKdb0VyF4VxI2ouY7+aoGVQNNdfbXDz610l1aPpY+Z 19 | CeY1G5dVDpwD0JGglWGvSfatbNBi4YRYo+6WD2725O/sYokZDDhCqcvtPSKJBPsg 20 | U2N56vyXfGegO366TalfQW7FncLNDhAUvP/lHm3xaA+gp2LsrJaX/1ouvh/4EGnX 21 | ojD1uUa3L1aNTZX0Hx7UJhahVXXS+EtWg952N6Ve73eF1uKGztPnqUZk5EBIJ1GN 22 | NHhYR33mo9BgIMWVekQgIAmC9gntvCYU++ParU/DpFh1i5U7oF/oLdlIlAYDWvgR 23 | v0vti4KulvU1UP4rk9kGlvRvlGta1f898f2rXEgW/PAKpXVaZgkdndfYSYe4t4Hf 24 | Ek34ISUIOJdG8FO/MYsPNvn26qqqaxBYfGDMkSca5TeAVzM= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /certificates/dummy_ven.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICojCCAYoCAQAwXTELMAkGA1UEBhMCTkwxDjAMBgNVBAgMBU90aGVyMRwwGgYD 3 | VQQKDBNPcGVuTEVBRFIgRHVtbXkgVkVOMSAwHgYDVQQDDBdkdW1teS12ZW4ub3Bl 4 | bmxlYWRyLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKg3/uws 5 | cG3+jkOWSrk4s6h4Hd1jIE7c73FH+2b0iRxb9pbBbU6LKnggNd3Sq+rAqUcL6/EH 6 | x7+vtBvvLSBu6xb5j2bDf1LboRet+Lg2Vof8cQavt7BhJ0XPEUrQ6GGv7FNfq8o0 7 | /+ao1IHS77LuWn7nNbjzdh0ZMhEoQpFh0KA9cTzUIUB0TTCSCMHsvOix8EVa0I/L 8 | K9KnlK8ebQU16wgK4Xvf3hjxqfkI5nza4EXsvY5pkJi5a3207Q1eORLiekOjgeU/ 9 | tVoHgqcZF79tZiyk7wwA3LIfqMnsd54FJocWeXxN17OAfhVAgrq1t14OU7EH35H8 10 | rGLSRZW8+v1rRA8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBh63cKWxRi4K+i 11 | RTSR9INQB+RL7tTZfv6DkPU87HYW2fuADC/MWMcIebeqdFS2LYoIsLmOoP2kKZlQ 12 | a6cvPZ80Pvm8fcKydtAYch8xJYcanFTJxXTHGHQycZ6uQxjR05KYXMe+lDo5YhCR 13 | CpBuaGC4+E0ZeX0vUh5Gl+0/qwaVyXeAyO27NqUILNOIgG9TgQo4AaaMG4XU5ICt 14 | s7HEAKUKe71hEt6BrNofVBSdiSQQ4WVxbt/XsfT2q6a6iPLba41GNE2bQeRowTJz 15 | V/UMtBCqGaaAFnMuJL9su/OuqhygXZLtH99oWyYH5Dq5JeLxc2KR5l8NCZL6lk/c 16 | wCVBu1Ln 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /certificates/dummy_ven.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAqDf+7Cxwbf6OQ5ZKuTizqHgd3WMgTtzvcUf7ZvSJHFv2lsFt 3 | TosqeCA13dKr6sCpRwvr8QfHv6+0G+8tIG7rFvmPZsN/UtuhF634uDZWh/xxBq+3 4 | sGEnRc8RStDoYa/sU1+ryjT/5qjUgdLvsu5afuc1uPN2HRkyEShCkWHQoD1xPNQh 5 | QHRNMJIIwey86LHwRVrQj8sr0qeUrx5tBTXrCArhe9/eGPGp+QjmfNrgRey9jmmQ 6 | mLlrfbTtDV45EuJ6Q6OB5T+1WgeCpxkXv21mLKTvDADcsh+oyex3ngUmhxZ5fE3X 7 | s4B+FUCCurW3Xg5TsQffkfysYtJFlbz6/WtEDwIDAQABAoIBAFvpPIAOR9/RlimX 8 | lHxfXspN1wN/hceRL8LVcadvNPspxDHavb6Mi0fXUZdB5Gz/l34aJXAssBcsCVy5 9 | 8g4mjIyhDpk3d/ntxrcJdzwvdFgYtijRGaxlaO/bk7ctLcsyNA3Z2CNDkg6VcIb+ 10 | mblKQmfULKZBX+fGPHTjanvE1hi2lrHnJE4Lkpqn7hHL2+Ev1dj6sbWJq+4CoI9Q 11 | 1HZoLB9B0YA8sfY5c8/G/3dEIKNN2z43+KIvoFAf5FeAe+W0R8AJcrqK6LnPjG8c 12 | p9EEgtbwVjrDIj+aqr+XgZYGDA6Rw12TpWHOsTfutvbexdNCf3Lm2azEf3nk3MKK 13 | 2HDhkgECgYEA1KSfTuJV0tKQOyMCs9kZsXPqQe1EekVer+CNPHw0qlbOdpB+Bru5 14 | /1BhSsjgqqJE8r68jsH+tzi2YBvF2ktrMXczbor0Czkxs2hDpzZAuWy/DEYO95KV 15 | 4CMQpELzbWfRYzUXLpWuN5GaRYBewATsyfNVt9H40InQ33RIwty0w/cCgYEAyoSM 16 | DifJI9hw7QIEecEKd/LzkbjAGxOzZippIHgZfB7y3GCkg385MkG+JeVeA3v5Pyv2 17 | vQTiCSWmjeuczpsyU6Jy6l+TBOsgzadRuspJllyzdgaB0PirHGy7yeETxGndx+1e 18 | ckklL1xM6P+eI5oLNorc2K6kJJwev8IACXzzyqkCgYAB+4Lstll8fLARjRMCYDzI 19 | Fb+SW8buqOToGNzYOoQ3LlYAbvptz6Q9SB+QFe1aSecAqFyTrCVWyfWRUdD3ZwqZ 20 | zHWzSyJZVHAtLwSqc2wJDoV9dM3A6yHwlAMctO8WDwi5tw3/Ri/4nqfki/zWJ3WZ 21 | sGYVL6T1NMO4wZnID0y0IwKBgHKAaqd+Oy03LPH0GA+243JaPPiBGqy0gNQc2n9v 22 | KAmTfgC364wlnHMb1KScgIE70Pq0orbQUfSWAwtu0aPG/7dlu89j5j37qvpbxcv0 23 | n5KSKy1qFG/QiP5zQ+GqjoCY0ro9LQ805/9VEm8SR5kdeYWHEcK5SzkfxArrZxX2 24 | PeixAoGBAJESwJps4MB0o50lSQkoququiD3E58gqAf5N4fLcxGEr3T0NX7WWQ2gg 25 | 6t0elrCPDiOzaHuaB2f7ArK7KPyVd5fRtSImk8MwQdL7lmzofFsvuq9X/Fc6krXj 26 | eqguWZfOoccEXRDtmCUI7Eagdd6oLt6WvDxsouGpU+ZENMOMY4Yo 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /certificates/dummy_vtn.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEMTCCAhkCFFAy/KOQUHcYvG617vnwctH01elBMA0GCSqGSIb3DQEBCwUAMFsx 3 | CzAJBgNVBAYTAk5MMQ4wDAYDVQQIDAVPdGhlcjEbMBkGA1UECgwST3BlbkxFQURS 4 | IER1bW15IENBMR8wHQYDVQQDDBZkdW1teS1jYS5vcGVubGVhZHIub3JnMB4XDTIy 5 | MDYxNjE4MjczOVoXDTMyMDYxMzE4MjczOVowTzELMAkGA1UEBhMCTkwxDjAMBgNV 6 | BAgMBU90aGVyMRwwGgYDVQQKDBNPcGVuTEVBRFIgRHVtbXkgVlROMRIwEAYDVQQD 7 | DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsEnYl 8 | eSlA01qfbpGw2Rz5JEWLOeN7iNFJfBVM7YDkwUDkNc6AkSJO99YYz6sdviB2G1wj 9 | 0bB8alVNhkCnJOLQteS37y0ma9k4w+E5FSxmGnGM8BToz1CXRZng6UBEzTpsAE7d 10 | 4JjX5LrI5R4wPTWOWBOA1u33V/9nBPAVHS2+MIfdnt7+bNHZa5oKopHC9tvb8N9t 11 | EiJ6FhMnVMxnlNorA1TTKTXEJifcSrTxcpdr+087wmbLnWxGImzP8UA5ZTDj5K7t 12 | iSzgY1EMWNhEdUoCcsKvKA5BJLkEtcKaUZzqiRhFySkMa6UFLBZKWnKttxi+a1rB 13 | r8opDr9s1bFIrmYPAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAEmjJLePLry/ZqY5 14 | O6iW1MlGznuuwMzDn9gnDai4a6Xzzow+AVUVD1SoswHiOBADuzqP2RPQD3sIYV9L 15 | dM14YBoei0QPtS7ZDJJ7dtK+MVxQlElX6T6nml01fguOmXG7aQdoLhxIzjUWjrL4 16 | +pGikg5dQ/5FwaFv20HtWqgqF+hQdI/v3//Hco71IKtH0I2riLsH8ZWjzvhlpFnf 17 | g0TAALyYIv2vr6TfXu+Xsf5ZTx0BOMgOBwsX1vo/LEoF4ML1/1uZe/DvcIIfmzVe 18 | vdHYuHQpyaibKuOTg6YjInQOE09vBDjXUr41Qb/IAmpV2rHvoybxvVI0EC3g5d9P 19 | 7vC7SwN5ADOR0Q1cL25KslXLrjYiOw08ugsunWQDsx8lr0kyC5KCy+mBNPtTOCCu 20 | v2tPNxepcCJv+Bp30PycKimaDhzU0FWrFQCuqVL1d2+ZcpSH8SS8yrDlcG36DdCf 21 | EsfBoU6kgOcpXWmcZXZ+UHiOnrb9yOfIe7nk8D2EyiCUDSgukKygH3gFNKV1JAtF 22 | qtRUf0P912tK0zd5q/llSg8X4d8NrM12qRfdi3v2YEFpDWbcCrDmgmN5ema4Q2nQ 23 | c/JEt4M0Ma3Sc4zEbzNw1yC/AxJL7o3Y5otuib5IG30OfwcpO5iy8DyRFUmAOvZR 24 | E2pu2LQGlXyBCRbnlP2g9q85Fjwl 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /certificates/dummy_vtn.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIClDCCAXwCAQAwTzELMAkGA1UEBhMCTkwxDjAMBgNVBAgMBU90aGVyMRwwGgYD 3 | VQQKDBNPcGVuTEVBRFIgRHVtbXkgVlROMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi 4 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsEnYleSlA01qfbpGw2Rz5JEWL 5 | OeN7iNFJfBVM7YDkwUDkNc6AkSJO99YYz6sdviB2G1wj0bB8alVNhkCnJOLQteS3 6 | 7y0ma9k4w+E5FSxmGnGM8BToz1CXRZng6UBEzTpsAE7d4JjX5LrI5R4wPTWOWBOA 7 | 1u33V/9nBPAVHS2+MIfdnt7+bNHZa5oKopHC9tvb8N9tEiJ6FhMnVMxnlNorA1TT 8 | KTXEJifcSrTxcpdr+087wmbLnWxGImzP8UA5ZTDj5K7tiSzgY1EMWNhEdUoCcsKv 9 | KA5BJLkEtcKaUZzqiRhFySkMa6UFLBZKWnKttxi+a1rBr8opDr9s1bFIrmYPAgMB 10 | AAGgADANBgkqhkiG9w0BAQsFAAOCAQEAJevbcHHFZGlmNVrXxowJ2h5CAq55qUj9 11 | AB2vJzJm0YGw+BFKpsP0aK7GsDw8AyafQstQ71Xazv7aPL06G9xhVCJcS606tmU0 12 | MN2+00MPw3ohTH+WIxCRqDayIa/Md1kKMmzfXTMsyn+qlKFLA4TculAWGwoQzDHb 13 | +SdqwZgU+hu218+jmtpohoWdqxA0RCaXoOHf4SxV8jfnOLRSHI/QJG6P9cDqe00f 14 | iDGerFHfcvXHObmxTQk5jrLf3j+aJ8IFQ+KXlNJoZqfsRVugjk1Q3edWimIcqreu 15 | vXzQ5UbLWoZ1wDOJlfcnl/zLTkXqKv1pQuUHHpXcbCP/K3ezpMOGXA== 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /certificates/dummy_vtn.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEArBJ2JXkpQNNan26RsNkc+SRFiznje4jRSXwVTO2A5MFA5DXO 3 | gJEiTvfWGM+rHb4gdhtcI9GwfGpVTYZApyTi0LXkt+8tJmvZOMPhORUsZhpxjPAU 4 | 6M9Ql0WZ4OlARM06bABO3eCY1+S6yOUeMD01jlgTgNbt91f/ZwTwFR0tvjCH3Z7e 5 | /mzR2WuaCqKRwvbb2/DfbRIiehYTJ1TMZ5TaKwNU0yk1xCYn3Eq08XKXa/tPO8Jm 6 | y51sRiJsz/FAOWUw4+Su7Yks4GNRDFjYRHVKAnLCrygOQSS5BLXCmlGc6okYRckp 7 | DGulBSwWSlpyrbcYvmtawa/KKQ6/bNWxSK5mDwIDAQABAoIBAFpf8+eopE8k8uLr 8 | 2t9MmRgNweznAaCfcnSrFDDsSRdcnO1/iS8jiyZ4qt0rLU+YzUnbAuDZQF2IJ7do 9 | /MoM6IgAENm+aDNWz6ct0jv97+zMlLkWW5UPVd+tsa1cNOIt/DI/UyzbyssRmjzr 10 | gZiKfXd+sPepayDO+hiskkA0rBLIY/dNhK98Tt6/5kHNO1Bn3L2lbel/YIvLexIe 11 | tMV5PtBFT0Dgf0rKr63LQruypSFdgwp9Rt8uclKaYsbH4rGoWykvseaa9zjQA86T 12 | HZZP/HglTEL+/biR8VYsMs3gz7ucv26n8SycN2YolneJKk9VCsLuyawXJRQ/PtdG 13 | +XbR8CECgYEA15Hvdsx9iof9jfrX73sSahLhXzqslJOXz0SyXWhD8rt1YFRcwkAd 14 | 0uKTXJFq89U3xjpJiMG0H6X5rup5kzHiP8Gfxab0w1Kd9tU7GISeUi7FuEZBXAzI 15 | I+PaylAqOF4PgOIjgbgKIVk6oubcSOwAP200p4oFD1qEP/3ouv6rQDECgYEAzFgU 16 | YEvtnCe5HKgU+BSYicCJPtxM1RMBYPIkmjTySMlUWV+s4Oi90F1NZ3Nsbkzp9A77 17 | vyIcKM6fTBqnfoyrPRogWy0QM0rx4AIgOXU7UGO1dOj2Zf16Ct4hlHkKpoHWBaT+ 18 | /AtE5ySdIRJLfml5qzamonoZMemeLSDGkKrGuj8CgYBgJIElfcxb9YzbPs4By+UJ 19 | lAQaAcQou67QTTYzvKXZY0vVO6rnI8tpW44Xke+ecjDe4u4a5TFdkBvMrFyujmf2 20 | wXdtoqm1V2qGRNGHfNZTlvKt3f+We0jj4OKaqqg53ZGSjMkDOL6j4vTo5IfIqiDu 21 | KNl8A22ATIGBPNAIXj6ocQKBgQC4fEzpI6PB95sIZDeKQg31T/6Y4gv70szL2dFx 22 | 55tWW23rwpUx+O5lz9ayL2MVshsGXQCr7v+9V845x1awyg9Peub7ahPWOzNkKoct 23 | WMnUKjEI+8p5Cf/FEAcBJMaYzV+MeQo8Q3BRvpyd3waJenB+QzjuN43HokNMOusK 24 | cSQ5wQKBgQCatzeFC7DwAGBKLLL4l8A8WwCUF/Vq3wh2/BaoRIETH6uaKIfdxcWd 25 | bjmHdaXib1krCrQjhXKzbJjsL/c/PrquaP80pN9u6UjweMw2YhcoXixSRdtPjr2V 26 | 7fLAQNzN7iLj/Jt1i+QfJ5opKGq8Qf3kqfkQ/SC46kZ1rhw8XgzgXQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /certificates/generate_certificates.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "Please make sure the line RANDFILE = ... is commented out in your /etc/ssl/openssl.conf." 4 | 5 | echo "Generating the CA key" 6 | openssl genrsa -out dummy_ca.key 4096 7 | 8 | echo "Generating the CA cert" 9 | openssl req -x509 -new -subj "/C=NL/ST=Other/O=OpenLEADR Dummy CA/CN=dummy-ca.openleadr.org" -nodes -key dummy_ca.key -sha256 -days 3650 -out dummy_ca.crt 10 | 11 | echo "Generating the VTN key" 12 | openssl genrsa -out dummy_vtn.key 2048 13 | 14 | echo "Generating the VTN Certificate Signing Request" 15 | openssl req -new -sha256 -key dummy_vtn.key -subj "/C=NL/ST=Other/O=OpenLEADR Dummy VTN/CN=localhost" -out dummy_vtn.csr 16 | 17 | echo "Signing the VTN CSR, generating the VTN certificate" 18 | openssl x509 -req -in dummy_vtn.csr -CA dummy_ca.crt -CAkey dummy_ca.key -CAcreateserial -out dummy_vtn.crt -days 3650 -sha256 19 | 20 | echo "Generating the VEN key" 21 | openssl genrsa -out dummy_ven.key 2048 22 | 23 | echo "Generating the VEN Certificate Signing Request" 24 | openssl req -new -sha256 -key dummy_ven.key -subj "/C=NL/ST=Other/O=OpenLEADR Dummy VEN/CN=dummy-ven.openleadr.org" -out dummy_ven.csr 25 | 26 | echo "Signing the VTN CSR, generating the VEN certificate" 27 | openssl x509 -req -in dummy_ven.csr -CA dummy_ca.crt -CAkey dummy_ca.key -CAcreateserial -out dummy_ven.crt -days 3650 -sha256 28 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | termcolor 2 | pytest 3 | pytest-asyncio 4 | sphinx 5 | sphinxcontrib-apidoc -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= ../python_env/bin/sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenLEADR/openleadr-python/dd171272d91e5cecbc27ecc95e74919f02484260/docs/_static/css/custom.css -------------------------------------------------------------------------------- /docs/_static/logo-tall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenLEADR/openleadr-python/dd171272d91e5cecbc27ecc95e74919f02484260/docs/_static/logo-tall.png -------------------------------------------------------------------------------- /docs/api/modules.rst: -------------------------------------------------------------------------------- 1 | openleadr 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | openleadr 8 | -------------------------------------------------------------------------------- /docs/api/openleadr.rst: -------------------------------------------------------------------------------- 1 | openleadr package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | openleadr.client module 8 | ----------------------- 9 | 10 | .. automodule:: openleadr.client 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | openleadr.enums module 16 | ---------------------- 17 | 18 | .. automodule:: openleadr.enums 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | openleadr.errors module 24 | ----------------------- 25 | 26 | .. automodule:: openleadr.errors 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | openleadr.fingerprint module 32 | ---------------------------- 33 | 34 | .. automodule:: openleadr.fingerprint 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | openleadr.hooks module 40 | ---------------------- 41 | 42 | .. automodule:: openleadr.hooks 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | openleadr.messaging module 48 | -------------------------- 49 | 50 | .. automodule:: openleadr.messaging 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | openleadr.objects module 56 | ------------------------ 57 | 58 | .. automodule:: openleadr.objects 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | openleadr.preflight module 64 | -------------------------- 65 | 66 | .. automodule:: openleadr.preflight 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | openleadr.server module 72 | ----------------------- 73 | 74 | .. automodule:: openleadr.server 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | openleadr.utils module 80 | ---------------------- 81 | 82 | .. automodule:: openleadr.utils 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | Module contents 88 | --------------- 89 | 90 | .. automodule:: openleadr 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'OpenLEADR' 21 | copyright = '2020, OpenLEADR Contributors' 22 | author = 'Stan Janssen' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'VERSION')) as file: 26 | release = file.read().strip() 27 | 28 | print(release) 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.apidoc'] 36 | 37 | apidoc_module_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'openleadr') 38 | apidoc_excluded_paths = [os.path.join(apidoc_module_dir, 'service'), 39 | os.path.join(apidoc_module_dir, 'config.py')] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 48 | 49 | autoclass_content = 'both' 50 | 51 | # -- Options for HTML output ------------------------------------------------- 52 | 53 | # The theme to use for HTML and HTML Help pages. See the documentation for 54 | # a list of builtin themes. 55 | # 56 | html_theme = 'alabaster' 57 | # html_logo = 'logo-tall.png' 58 | html_theme_options = { 59 | 'logo': 'logo-tall.png', 60 | 'logo_name': False, 61 | 'github_button': 'true', 62 | 'github_type': 'star', 63 | 'github_count': 'true', 64 | 'github_user': 'openleadr', 65 | 'github_repo': 'openleadr-python', 66 | 'font_family': 'sans-serif', 67 | 'font_size': 8 68 | } 69 | 70 | # Add any paths that contain custom static files (such as style sheets) here, 71 | # relative to this directory. They are copied after the builtin static files, 72 | # so a file named "default.css" will overwrite the builtin "default.css". 73 | html_static_path = ['_static'] 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/features.rst: -------------------------------------------------------------------------------- 1 | .. _feature_tour: 2 | 3 | ############ 4 | Feature Tour 5 | ############ 6 | 7 | Automatic registration 8 | ---------------------- 9 | 10 | Registering your VEN to a VTN is a process that requires some service discovery, exchange of reporting capabilitiets, assignment of a VEN_ID, et cetera. You don't have to see any of this in your own code. Just create a new OpenADRClient instance, provide a VTN URL, a VEN name, optionally provide signing certificates and a verification fingerprint, and registration happens automatically. 11 | 12 | 13 | Automatic reporting 14 | ------------------- 15 | 16 | If you're implementing a VEN (client), you configure which types of reports you can offer, you provide handlers that retrieve the measurements, and OpenLEADR will take care of the rest. It will offer the reports to the VTN, allow the VTN to pick the reports it requires, and it will call your data collection handlers at a set interval, and pack up the values into the reports. 17 | 18 | If you're implementing a VTN (server), you configure which types of reports you wish to receive, and it will automatically request these reports from the VEN. Whenever new data arrives, it will call one of your handlers so you can hand the data off to your own systems for processing and archiving. 19 | 20 | 21 | Event-driven 22 | ------------ 23 | 24 | Once you have configured your client or server, all routine interactions happen automatically: polling for new Events, requesting reports, et cetera. Whenever OpenLEADR needs some information or data that it does not have, it will call your handlers in order to get it. Your handlers only have to deal with a very specific set of cases and they can usually be quite simple. 25 | 26 | 27 | Dict-style representations 28 | -------------------------- 29 | 30 | Although OpenADR is an XML-based protocol that is either transported over HTTP or XMPP, you don't need to see any XML or custom object types at all. You use native Python dicts (and native Python types like `datetime.datetime` and `datetime.timedelta`) in your handlers. 31 | 32 | 33 | Message Signing for secure communications 34 | ----------------------------------------- 35 | 36 | If you provide a PEM-formatted certificate and key, all outgoing messages will be cryptographically signed. If you also provide a fingerprint for the other side's certificate, incoming messages can be securely authenticated and verified. 37 | 38 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. OpenLEADR documentation master file, created by 2 | sphinx-quickstart on Thu Jul 9 14:09:27 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ==================== 7 | Welcome to OpenLEADR 8 | ==================== 9 | 10 | A friendly and compliant OpenADR implementation for Python 3. 11 | 12 | Key Features 13 | ============ 14 | 15 | - Fully compliant OpenADR 2.0b implementation for both servers (Virtual Top Node) and clients (Virtual End Node) 16 | - Fully asyncio: you set up the coroutines that can handle certain events, and they get called when needed. 17 | - Fully pythonic: all messages are represented as simple Python dictionaries. All XML parsing and generation is done for you. 18 | 19 | Take a :ref:`feature_tour`! 20 | 21 | Project Status 22 | ============== 23 | 24 | The current version is |release|. 25 | 26 | This project is still a work in progress. Please see our :ref:`roadmap` for information. 27 | 28 | License 29 | ======= 30 | 31 | This project is licensed under the Apache 2.0 license. 32 | 33 | Development and contributing 34 | ============================ 35 | 36 | The source code of this project can be found on `GitHub `_. Feature requests, bug reports and pull requests are most welcome and can be posted there. 37 | 38 | Library Installation 39 | ==================== 40 | 41 | .. code-block:: bash 42 | 43 | $ pip install openleadr 44 | 45 | OpenLEADR is compatible with Python 3.7 and higher. 46 | 47 | Getting Started 48 | =============== 49 | 50 | Client example:: 51 | 52 | from openleadr import OpenADRClient 53 | import asyncio 54 | 55 | async def main(): 56 | client = OpenADRClient(ven_name="Device001", 57 | vtn_url="http://localhost:8080/OpenADR2/Simple/2.0b") 58 | client.add_handler('on_event', handle_event) 59 | await client.run() 60 | 61 | async def handle_event(event): 62 | """ 63 | This coroutine will be called 64 | when there is an event to be handled. 65 | """ 66 | print("There is an event!") 67 | print(event) 68 | # Do something to determine whether to Opt In or Opt Out 69 | return 'optIn' 70 | 71 | loop = asyncio.get_event_loop() 72 | loop.create_task(main()) 73 | loop.run_forever() 74 | 75 | This will connect to an OpenADR server (indicated by the vtn_url parameter), handle registration, start polling for events and reports, and will call your coroutines whenever an event or report is created for you. 76 | 77 | 78 | 79 | Table of Contents 80 | ================= 81 | 82 | .. toctree:: 83 | :name: mastertoc 84 | :maxdepth: 2 85 | 86 | features 87 | client 88 | server 89 | reporting 90 | logging 91 | message_signing 92 | roadmap 93 | API Reference 94 | representations 95 | 96 | Representations of OpenADR payloads 97 | =================================== 98 | 99 | OpenLEADR uses Python dictionaries and vanilla Python types (like datetime and timedelta) to represent the OpenADR payloads. These pages serve as a reference to these representations. 100 | 101 | For example, this XML payload: 102 | 103 | .. code-block:: xml 104 | 105 | 106 | 107 | 108 | 109 | 110 | 200 111 | OK 112 | 113 | 114 | o57b65be83 115 | 116 | 117 | 118 | 119 | is represented in OpenLEADR as: 120 | 121 | .. code-block:: python3 122 | 123 | {'response': {'response_code': 200, 124 | 'response_description': 'OK'}, 125 | 'ven_id': 'o57b65be83'} 126 | 127 | Read more about the representations at :ref:`representations` 128 | 129 | 130 | Indices and tables 131 | ================== 132 | 133 | * :ref:`genindex` 134 | * :ref:`modindex` 135 | * :ref:`search` 136 | -------------------------------------------------------------------------------- /docs/logging.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Logging 3 | ======= 4 | 5 | OpenLEADR uses the standard Python Logging facility. Following the `Python guidelines `_, no default handlers are configured, but you can easily get going if you want to: 6 | 7 | Log to standard output 8 | ---------------------- 9 | 10 | To print logs to your standard output, you can use the following convenience method: 11 | 12 | .. code-block:: python3 13 | 14 | import openleadr 15 | import logging 16 | openleadr.enable_default_logging(level=logging.INFO) 17 | 18 | Which is the same as: 19 | 20 | .. code-block:: python3 21 | 22 | import logging 23 | logger = logging.getLogger('openleadr') 24 | handler = logging.StreamHandler() 25 | handler.setLevel(logging.INFO) 26 | logger.addHandler(handler) 27 | 28 | 29 | Setting the logging level 30 | ------------------------- 31 | 32 | You can set different logging levels for the logging generated by OpenLEADR: 33 | 34 | .. code-block:: python3 35 | 36 | import logging 37 | logger = logging.getLogger('openleadr') 38 | logger.setLevel(logging.WARNING) 39 | 40 | The different `logging levels `_ are: 41 | 42 | .. code-block:: python3 43 | 44 | logging.DEBUG 45 | logging.INFO 46 | logging.WARNING 47 | logging.ERROR 48 | logging.CRITICAL 49 | 50 | Redirecting logs to a different file 51 | ------------------------------------ 52 | 53 | To write logs to a file: 54 | 55 | .. code-block:: python3 56 | 57 | import logging 58 | logger = logging.getLogger('openleadr') 59 | 60 | handler = logging.FileHandler('mylogfile.txt') 61 | logger.addHandler(logger) 62 | 63 | More info 64 | --------- 65 | 66 | Detailed info on logging configuration can be found on `the official Python documentation `_. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/message_signing.rst: -------------------------------------------------------------------------------- 1 | .. _message_signing: 2 | 3 | =============== 4 | Message Signing 5 | =============== 6 | 7 | OpenADR messages should ideally be signed using an X509 certificate keypair. This allows both parties to verify that the message came from the correct party, and that it was not tampered with in transport. It does not provide message encryption; for that, a transport-level encryption (TLS) should be used. 8 | 9 | Overview 10 | -------- 11 | 12 | The high level overview is this: 13 | 14 | 1. The VTN creates an X509 certificate and an associated private key. It shares a fingerprint of the certificate with the VEN it connects to (this fingerprint will be printed to your console on startup). 15 | 2. The VEN receives a signed (not encrypted) message from the VTN. Each message to the VEN includes the complete X509 Certificate that can be used to verify the signature. The VEN verifies that the message signature is correct, and it verifies that the certificate fingerprint matches the fingerprint that it received from the VEN. 16 | 17 | The same process applies with the parties reversed. 18 | 19 | For a VEN (client), which talks to one VTN, you simply provide the path to the signing certificate and private key, and the private key passphrase and the VTN's fingerprint on initialization. See: :ref:`client_signing_messages` and :ref:`client_validating_messages`. 20 | 21 | For a VTN (server), which talks to multiple VENs, you provide the signing certificate, private key, private key passphrase and a handler function that can look up the certificate fingerprint when given a ven_id. See: :ref:`server_signing_messages`. 22 | 23 | 24 | Generating certificates 25 | ----------------------- 26 | 27 | To generate a certificate + private key pair, you can use the following command on Linux: 28 | 29 | .. code-block:: text 30 | 31 | openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 32 | 33 | This will generate two files (key.pem and cert.pem) and require you to input a passphrase that encrypts/decrypts the private key. 34 | 35 | You can provide paths to these files, as well as the passphrase, in your Client or Server initialization. See the examples referred to above. 36 | 37 | 38 | Replay Protection 39 | ----------------- 40 | 41 | To prevent an attacker from simple re-playing a previous message (for instance, an Event), unmodified, each signed message contains a ReplayProtect property. This contains a random string (nonce) and a timestamp. Upon validation of the message, it is verified that the timestamp is not older then some preconfigured value (default: 5 seconds), and that the random string has not already been seen during that time window. A cache of the nonces is kept automatically to verify this. 42 | 43 | OpenLEADR automatically generates and validates these portions of the signature. Signed messages that do not contain a ReplayProtect element are rejected, as required by the OpenADR specification. 44 | 45 | 46 | Certificate Fingerprints 47 | ------------------------ 48 | 49 | In order for the opposing party to verify that you ar who you say you are, they need the fingerprint of your public key that you are using. You can get this fingerprint in two ways: 50 | 51 | 1. It will be printed to your stdout when you start your client or server. If you don't want this, your can turn it off by passing `show_fingerprint=False` to the OpenADRClient() or OpenADRServer() constructors. 52 | 2. You can use an included commandline tool to show the fingerprint of any PEM certificate: 53 | 54 | .. code-block:: bash 55 | 56 | pip3 install --user openleadr 57 | fingerprint path/to/cert.pem 58 | 59 | The fingerprint is not sensitive material; you can distribute it via an unsecured method, or you can publish it as a DNS-record for example. 60 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenLEADR/openleadr-python/dd171272d91e5cecbc27ecc95e74919f02484260/logo.png -------------------------------------------------------------------------------- /openleadr/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # flake8: noqa 18 | 19 | import logging 20 | from .client import OpenADRClient 21 | from .server import OpenADRServer 22 | 23 | 24 | def enable_default_logging(level=logging.INFO): 25 | """ 26 | Turn on logging to stdout. 27 | :param level integer: The logging level you wish to use. 28 | Defaults to logging.INFO. 29 | """ 30 | import sys 31 | import logging 32 | logger = logging.getLogger('openleadr') 33 | handler_names = [handler.name for handler in logger.handlers] 34 | if 'openleadr_default_handler' not in handler_names: 35 | logger.setLevel(level) 36 | logging_handler = logging.StreamHandler(stream=sys.stdout) 37 | logging_handler.set_name('openleadr_default_handler') 38 | logging_handler.setLevel(logging.DEBUG) 39 | logger.addHandler(logging_handler) 40 | -------------------------------------------------------------------------------- /openleadr/errors.py: -------------------------------------------------------------------------------- 1 | from openleadr.enums import STATUS_CODES 2 | 3 | 4 | class ProtocolError(Exception): 5 | pass 6 | 7 | 8 | class FingerprintMismatch(Exception): 9 | pass 10 | 11 | 12 | class HTTPError(Exception): 13 | def __init__(self, status=500, description=None): 14 | super().__init__() 15 | self.response_code = status 16 | self.response_description = description 17 | 18 | 19 | class OutOfSequenceError(ProtocolError): 20 | def __init__(self, description='OUT OF SEQUENCE'): 21 | super().__init__() 22 | self.response_code = STATUS_CODES.OUT_OF_SEQUENCE 23 | self.response_description = description 24 | 25 | 26 | class NotAllowedError(ProtocolError): 27 | def __init__(self, description='NOT ALLOWED'): 28 | super().__init__() 29 | self.response_code = STATUS_CODES.NOT_ALLOWED 30 | self.response_description = description 31 | 32 | 33 | class InvalidIdError(ProtocolError): 34 | def __init__(self, description='INVALID ID'): 35 | super().__init__() 36 | self.response_code = STATUS_CODES.INVALID_ID 37 | self.response_description = description 38 | 39 | 40 | class NotRecognizedError(ProtocolError): 41 | def __init__(self, description='NOT RECOGNIZED'): 42 | super().__init__() 43 | self.response_code = STATUS_CODES.NOT_RECOGNIZED 44 | self.response_description = description 45 | 46 | 47 | class InvalidDataError(ProtocolError): 48 | def __init__(self, description='INVALID DATA'): 49 | super().__init__() 50 | self.response_code = STATUS_CODES.INVALID_DATA 51 | self.response_description = description 52 | 53 | 54 | class ComplianceError(ProtocolError): 55 | def __init__(self, description='COMPLIANCE ERROR'): 56 | super().__init__() 57 | self.response_code = STATUS_CODES.COMPLIANCE_ERROR 58 | self.response_description = description 59 | 60 | 61 | class SignalNotSupportedError(ProtocolError): 62 | def __init__(self, description='SIGNAL NOT SUPPORTED'): 63 | super().__init__() 64 | self.response_code = STATUS_CODES.SIGNAL_NOT_SUPPORTED 65 | self.response_description = description 66 | 67 | 68 | class ReportNotSupportedError(ProtocolError): 69 | def __init__(self, description='REPORT NOT SUPPORTED'): 70 | super().__init__() 71 | self.response_code = STATUS_CODES.REPORT_NOT_SUPPORTED 72 | self.response_description = description 73 | 74 | 75 | class TargetMismatchError(ProtocolError): 76 | def __init__(self, description='TARGET MISMATCH'): 77 | super().__init__() 78 | self.response_code = STATUS_CODES.TARGET_MISMATCH 79 | self.response_description = description 80 | 81 | 82 | class NotRegisteredOrAuthorizedError(ProtocolError): 83 | def __init__(self, description='NOT REGISTERED OR AUTHORIZED'): 84 | super().__init__() 85 | self.response_code = STATUS_CODES.NOT_REGISTERED_OR_AUTHORIZED 86 | self.response_description = description 87 | 88 | 89 | class DeploymentError(ProtocolError): 90 | def __init__(self, description='DEPLOYMENT ERROR OR OTHER ERROR'): 91 | super().__init__() 92 | self.response_code = STATUS_CODES.DEPLOYMENT_ERROR_OR_OTHER_ERROR 93 | self.response_description = description 94 | 95 | 96 | # Flow-control based Exceptions 97 | class RequestReregistration(Exception): 98 | def __init__(self, ven_id=None): 99 | super().__init__() 100 | self.ven_id = ven_id 101 | 102 | 103 | class SendEmptyHTTPResponse(Exception): 104 | pass 105 | -------------------------------------------------------------------------------- /openleadr/fingerprint.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from openleadr.utils import certificate_fingerprint 3 | 4 | 5 | def show_fingerprint(): 6 | parser = ArgumentParser() 7 | parser.add_argument('certificate', type=str) 8 | args = parser.parse_args() 9 | 10 | if 'certificate' in args: 11 | with open(args.certificate) as file: 12 | cert_str = file.read() 13 | print(certificate_fingerprint(cert_str)) 14 | 15 | 16 | if __name__ == "__main__": 17 | show_fingerprint() 18 | -------------------------------------------------------------------------------- /openleadr/hooks.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import asyncio 18 | 19 | HOOKS = {'before_parse': [], 20 | 'before_handle': [], 21 | 'after_handle': [], 22 | 'before_respond': []} 23 | 24 | 25 | def register(hook_point, callback): 26 | """ 27 | Call a hook 28 | """ 29 | if hook_point not in HOOKS: 30 | raise ValueError(f"""The hook_point must be one of '{', '.join(HOOKS.keys())}', """ 31 | f"""you provided '{hook_point}'""") 32 | HOOKS[hook_point].append(callback) 33 | 34 | 35 | def call(hook_point, *args, **kwargs): 36 | loop = asyncio.get_event_loop() 37 | for hook in HOOKS.get(hook_point, []): 38 | loop.create_task(hook(*args, **kwargs)) 39 | -------------------------------------------------------------------------------- /openleadr/schema/LICENSES.txt: -------------------------------------------------------------------------------- 1 | This file contains the license information for some of the included XSD files, 2 | which are owned by others. 3 | 4 | oadr_xmldsig11.xsd: 5 | Licensed under the W3C license. The W3C license agreement is copied below. 6 | 7 | oadr_siscale_20b.xsd 8 | This file is © OASIS Open 2012 and contains a copyright notice. The text of 9 | that notice is copied below. 10 | 11 | oadr_ISO_ISO3AlphaCurrencyCode_20100407.xsd 12 | Licensed under the UN_CEFACT license. The license agreement is listed inside 13 | that file. 14 | 15 | Other XSDs included in this directory do not contain explicit license 16 | information, and are included here in good faith and believing that we are 17 | allowed allowed to include them here. If you feel like we are missing an 18 | attribution, we will do everything we can to correct this problem. Please reach 19 | out to us at team@openleadr.org or file an issue on our GitHub page at 20 | https://github.com/openleadr/openleadr-python/issues. Thank you. 21 | 22 | 23 | ================================= W3C Licence ================================= 24 | 25 | This work is being provided by the copyright holders under the following 26 | license. License 27 | 28 | By obtaining and/or copying this work, you (the licensee) agree that you have 29 | read, understood, and will comply with the following terms and conditions. 30 | 31 | Permission to copy, modify, and distribute this work, with or without 32 | modification, for any purpose and without fee or royalty is hereby granted, 33 | provided that you include the following on ALL copies of the work or portions 34 | thereof, including modifications: 35 | 36 | The full text of this NOTICE in a location viewable to users of the 37 | redistributed or derivative work. Any pre-existing intellectual property 38 | disclaimers, notices, or terms and conditions. If none exist, the W3C 39 | Software and Document Short Notice should be included. Notice of any changes 40 | or modifications, through a copyright statement on the new code or document 41 | such as "This software or document includes material copied from or derived 42 | from [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, 43 | ERCIM, Keio, Beihang)." 44 | 45 | Disclaimers 46 | 47 | THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR 48 | WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF 49 | MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE 50 | SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, 51 | TRADEMARKS OR OTHER RIGHTS. 52 | 53 | COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR 54 | CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. 55 | 56 | The name and trademarks of copyright holders may NOT be used in advertising or 57 | publicity pertaining to the work without specific, written prior permission. 58 | Title to copyright in this work will at all times remain with copyright holders. 59 | 60 | ------------------------------------------------------------------------------- 61 | 62 | 63 | ========================= OASIS Open Copyright Notice ========================= 64 | 65 | Copyright © OASIS Open 2012. All Rights Reserved. 66 | 67 | All capitalized terms in the following text have the meanings assigned to them 68 | in the OASIS Intellectual Property Rights Policy (the "OASIS IPR Policy"). The 69 | full Policy may be found at the OASIS website. 70 | 71 | This document and translations of it may be copied and furnished to others, and 72 | derivative works that comment on or otherwise explain it or assist in its 73 | implementation may be prepared, copied, published, and distributed, in whole or 74 | in part, without restriction of any kind, provided that the above copyright 75 | notice and this section are included on all such copies and derivative works. 76 | However, this document itself may not be modified in any way, including by 77 | removing the copyright notice or references to OASIS, except as needed for the 78 | purpose of developing any document or deliverable produced by an OASIS Technical 79 | Committee (in which case the rules applicable to copyrights, as set forth in the 80 | OASIS IPR Policy, must be followed) or as required to translate it into 81 | languages other than English. 82 | 83 | The limited permissions granted above are perpetual and will not be revoked by 84 | OASIS or its successors or assigns. 85 | 86 | This document and the information contained herein is provided on an "AS IS" 87 | basis and OASIS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT 88 | LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE 89 | ANY OWNERSHIP RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR 90 | A PARTICULAR PURPOSE. 91 | 92 | OASIS requests that any OASIS Party or any other party that believes it has 93 | patent claims that would necessarily be infringed by implementations of this 94 | OASIS Committee Specification or OASIS Standard, to notify OASIS TC 95 | Administrator and provide an indication of its willingness to grant patent 96 | licenses to such patent claims in a manner consistent with the IPR Mode of the 97 | OASIS Technical Committee that produced this specification. 98 | 99 | OASIS invites any party to contact the OASIS TC Administrator if it is aware of 100 | a claim of ownership of any patent claims that would necessarily be infringed by 101 | implementations of this specification by a patent holder that is not willing to 102 | provide a license to such patent claims in a manner consistent with the IPR Mode 103 | of the OASIS Technical Committee that produced this specification. OASIS may 104 | include such claims on its website, but disclaims any obligation to do so. 105 | 106 | OASIS takes no position regarding the validity or scope of any intellectual 107 | property or other rights that might be claimed to pertain to the implementation 108 | or use of the technology described in this document or the extent to which any 109 | license under such rights might or might not be available; neither does it 110 | represent that it has made any effort to identify any such rights. Information 111 | on OASIS' procedures with respect to rights in any document or deliverable 112 | produced by an OASIS Technical Committee can be found on the OASIS website. 113 | Copies of claims of rights made available for publication and any assurances of 114 | licenses to be made available, or the result of an attempt made to obtain a 115 | general license or permission for the use of such proprietary rights by 116 | implementers or users of this OASIS Committee Specification or OASIS Standard, 117 | can be obtained from the OASIS TC Administrator. OASIS makes no representation 118 | that any information or list of intellectual property rights will at any time be 119 | complete, or that any claims in such list are, in fact, Essential Claims. 120 | 121 | The name "OASIS" is a trademark of OASIS, the owner and developer of this 122 | specification, and should be used only to refer to the organization and its 123 | official outputs. OASIS welcomes reference to, and implementation and use of, 124 | specifications, while reserving the right to enforce its marks against 125 | misleading uses. Please see http://www.oasis-open.org/who/trademark.php for 126 | above guidance. 127 | 128 | ------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /openleadr/schema/oadr_emix_20b.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A URI identifying a DR Program 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | The Service Area is the geographic region that is affected by the EMIX market condition 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Abstract base type for units for EMIX Product delivery, measurement, and warrants. 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_gml_20b.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_pyld_20b.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | A ID used to match up a logical transaction request and response 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Request Event from a VTN in pull mode 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Limit the number of events contained in the oadrDistributeEvent payload 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Respond to a DR Event with optIn or optOut 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Indicates if report (in the form of UpdateReport) to be returned following cancellation of Report 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_siscale_20b.xsd: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Scale based on representations of SI scale as expressed in the unit multipliers 35 | enumeration 36 | 37 | 38 | 39 | 40 | Pico 10**-12 41 | -12 42 | 43 | 44 | 45 | 46 | Nano 10**-9 47 | -9 48 | 49 | 50 | 51 | 52 | Micro 10**-6 53 | -6 54 | 55 | 56 | 57 | 58 | Milli 10**-3 59 | -3 60 | 61 | 62 | 63 | 64 | Centi 10**-2 65 | -2 66 | 67 | 68 | 69 | 70 | Deci 10**-1 71 | -1 72 | 73 | 74 | 75 | 76 | Kilo 10**3 77 | 3 78 | 79 | 80 | 81 | 82 | Mega 10**6 83 | 6 84 | 85 | 86 | 87 | 88 | Giga 10**9 89 | 9 90 | 91 | 92 | 93 | 94 | Tera 10**12 95 | 12 96 | 97 | 98 | 99 | 100 | Native Scale 101 | 0 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_strm_20b.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Time intervals during which the DR event is active or report data is available 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Abstract class to convey a payload for a stream. When a Stream is transformed to or from a WS-Calendar Interval, the contents of the Stream Payload defined element are transformed into the contents of a WS-Calendar artifactBase. 22 | 23 | 24 | 25 | 26 | 27 | 28 | abstract base for communication of schedules for signals and observations 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_xcal_20b.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | The duration of the activity, data, or state 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Used as an index to identify intervals 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | The starting time for the activity, data, or state change 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Set randomization period for start of event 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | A schedule reflecting device availability for participating in DR events 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | An interval takes no sub-components. 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_xml.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | See http://www.w3.org/XML/1998/namespace.html and 6 | http://www.w3.org/TR/REC-xml for information about this namespace. 7 | 8 | This schema document describes the XML namespace, in a form 9 | suitable for import by other schema documents. 10 | 11 | Note that local names in this namespace are intended to be defined 12 | only by the World Wide Web Consortium or its subgroups. The 13 | following names are currently defined in this namespace and should 14 | not be used with conflicting semantics by any Working Group, 15 | specification, or document instance: 16 | 17 | base (as an attribute name): denotes an attribute whose value 18 | provides a URI to be used as the base for interpreting any 19 | relative URIs in the scope of the element on which it 20 | appears; its value is inherited. This name is reserved 21 | by virtue of its definition in the XML Base specification. 22 | 23 | lang (as an attribute name): denotes an attribute whose value 24 | is a language code for the natural language of the content of 25 | any element; its value is inherited. This name is reserved 26 | by virtue of its definition in the XML specification. 27 | 28 | space (as an attribute name): denotes an attribute whose 29 | value is a keyword indicating what whitespace processing 30 | discipline is intended for the content of the element; its 31 | value is inherited. This name is reserved by virtue of its 32 | definition in the XML specification. 33 | 34 | Father (in any context at all): denotes Jon Bosak, the chair of 35 | the original XML Working Group. This name is reserved by 36 | the following decision of the W3C XML Plenary and 37 | XML Coordination groups: 38 | 39 | In appreciation for his vision, leadership and dedication 40 | the W3C XML Plenary on this 10th day of February, 2000 41 | reserves for Jon Bosak in perpetuity the XML name 42 | xml:Father 43 | 44 | 45 | 46 | This schema defines attributes and an attribute group 47 | suitable for use by 48 | schemas wishing to allow xml:base, xml:lang or xml:space attributes 49 | on elements they define. 50 | 51 | To enable this, such a schema must import this schema 52 | for the XML namespace, e.g. as follows: 53 | <schema . . .> 54 | . . . 55 | <import namespace="http://www.w3.org/XML/1998/namespace" 56 | schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> 57 | 58 | Subsequently, qualified reference to any of the attributes 59 | or the group defined below will have the desired effect, e.g. 60 | 61 | <type . . .> 62 | . . . 63 | <attributeGroup ref="xml:specialAttrs"/> 64 | 65 | will define a type which will schema-validate an instance 66 | element with any of those attributes 67 | 68 | 69 | In keeping with the XML Schema WG's standard versioning 70 | policy, this schema document will persist at 71 | http://www.w3.org/2001/03/xml.xsd. 72 | At the date of issue it can also be found at 73 | http://www.w3.org/2001/xml.xsd. 74 | The schema document at that URI may however change in the future, 75 | in order to remain compatible with the latest version of XML Schema 76 | itself. In other words, if the XML Schema namespace changes, the version 77 | of this document at 78 | http://www.w3.org/2001/xml.xsd will change 79 | accordingly; the version at 80 | http://www.w3.org/2001/03/xml.xsd will not change. 81 | 82 | 83 | 84 | 85 | In due course, we should install the relevant ISO 2- and 3-letter 86 | codes as the enumerated possible values . . . 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | See http://www.w3.org/TR/xmlbase/ for 100 | information about this attribute. 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_xmldsig-properties-schema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /openleadr/schema/oadr_xmldsig11.xsd: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /openleadr/service/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # flake8: noqa 18 | 19 | from .decorators import handler, service 20 | from .vtn_service import VTNService 21 | from .event_service import EventService 22 | from .poll_service import PollService 23 | from .registration_service import RegistrationService 24 | from .report_service import ReportService 25 | from .opt_service import OptService 26 | -------------------------------------------------------------------------------- /openleadr/service/decorators.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | def handler(message_type): 18 | """ 19 | Decorator to mark a method as the handler for a specific message type. 20 | """ 21 | def _actual_decorator(decorated_function): 22 | decorated_function.__message_type__ = message_type 23 | return decorated_function 24 | return _actual_decorator 25 | 26 | 27 | def service(service_name): 28 | """ 29 | Decorator to mark a class as an OpenADR Service for a specific endpoint. 30 | """ 31 | def _actual_decorator(decorated_function): 32 | decorated_function.__service_name__ = service_name 33 | return decorated_function 34 | return _actual_decorator 35 | -------------------------------------------------------------------------------- /openleadr/service/event_service.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from . import service, handler, VTNService 18 | import asyncio 19 | from openleadr import utils, errors, enums 20 | import logging 21 | logger = logging.getLogger('openleadr') 22 | 23 | 24 | @service('EiEvent') 25 | class EventService(VTNService): 26 | 27 | def __init__(self, vtn_id, polling_method='internal'): 28 | super().__init__(vtn_id) 29 | self.polling_method = polling_method 30 | self.events = {} 31 | self.completed_event_ids = {} # Holds the ids of completed events 32 | self.event_callbacks = {} 33 | self.event_opt_types = {} 34 | self.event_delivery_callbacks = {} 35 | 36 | @handler('oadrRequestEvent') 37 | async def request_event(self, payload): 38 | """ 39 | The VEN requests us to send any events we have. 40 | """ 41 | ven_id = payload['ven_id'] 42 | if self.polling_method == 'internal': 43 | if ven_id in self.events and self.events[ven_id]: 44 | events = utils.order_events(self.events[ven_id]) 45 | for event in events: 46 | event_status = utils.getmember(event, 'event_descriptor.event_status') 47 | # Pop the event from the events so that this is the last time it is communicated 48 | if event_status == enums.EVENT_STATUS.COMPLETED: 49 | if ven_id not in self.completed_event_ids: 50 | self.completed_event_ids[ven_id] = [] 51 | event_id = utils.getmember(event, 'event_descriptor.event_id') 52 | self.completed_event_ids[ven_id].append(event_id) 53 | self.events[ven_id].pop(self.events[ven_id].index(event)) 54 | else: 55 | events = None 56 | else: 57 | result = self.on_request_event(ven_id=payload['ven_id']) 58 | if asyncio.iscoroutine(result): 59 | result = await result 60 | if result is None: 61 | events = None 62 | else: 63 | events = utils.order_events(result) 64 | 65 | if events is None: 66 | return 'oadrResponse', {} 67 | else: 68 | # Fire the delivery callbacks, if any 69 | for event in events: 70 | event_id = utils.getmember(event, 'event_descriptor.event_id') 71 | if event_id in self.event_delivery_callbacks: 72 | await utils.await_if_required(self.event_delivery_callbacks[event_id]()) 73 | return 'oadrDistributeEvent', {'events': events} 74 | return 'oadrResponse', result 75 | 76 | def on_request_event(self, ven_id): 77 | """ 78 | Placeholder for the on_request_event handler. 79 | """ 80 | logger.warning("You should implement and register your own on_request_event handler " 81 | "that returns the next event for a VEN. This handler will receive a " 82 | "ven_id as its only argument, and should return None (if no events are " 83 | "available), a single Event, or a list of Events.") 84 | return None 85 | 86 | @handler('oadrCreatedEvent') 87 | async def created_event(self, payload): 88 | """ 89 | The VEN informs us that they created an EiEvent. 90 | """ 91 | ven_id = payload['ven_id'] 92 | if self.polling_method == 'internal': 93 | for event_response in payload['event_responses']: 94 | event_id = event_response['event_id'] 95 | modification_number = event_response['modification_number'] 96 | opt_type = event_response['opt_type'] 97 | event = utils.find_by(self.events[ven_id], 98 | 'event_descriptor.event_id', event_id, 99 | 'event_descriptor.modification_number', modification_number) 100 | if not event: 101 | if event_id not in self.completed_event_ids.get(ven_id, []): 102 | logger.warning(f"""Got an oadrCreatedEvent message from ven '{ven_id}' """ 103 | f"""for event '{event_id}' with modification number """ 104 | f"""{modification_number} that does not exist.""") 105 | raise errors.InvalidIdError 106 | # Remove the event from the events list if the cancellation is confirmed. 107 | if utils.getmember(event, 'event_descriptor.event_status') == enums.EVENT_STATUS.CANCELLED: 108 | utils.pop_by(self.events[ven_id], 'event_descriptor.event_id', event_id) 109 | if event_response['event_id'] in self.event_callbacks: 110 | event, callback = self.event_callbacks.pop(event_id) 111 | if isinstance(callback, asyncio.Future): 112 | if callback.done(): 113 | logger.warning(f"Got a second response '{opt_type}' from ven '{ven_id}' " 114 | f"to event '{event_id}', which we cannot use because the " 115 | "callback future you provided was already completed during " 116 | "the first response.") 117 | else: 118 | callback.set_result(opt_type) 119 | else: 120 | result = callback(ven_id=ven_id, event_id=event_id, opt_type=opt_type) 121 | if asyncio.iscoroutine(result): 122 | result = await result 123 | else: 124 | for event_response in payload['event_responses']: 125 | event_id = event_response['event_id'] 126 | opt_type = event_response['opt_type'] 127 | result = await utils.await_if_required(self.on_created_event(ven_id=ven_id, 128 | event_id=event_id, 129 | opt_type=opt_type)) 130 | return 'oadrResponse', {} 131 | 132 | def on_created_event(self, ven_id, event_id, opt_type): 133 | """ 134 | Placeholder for the on_created_event handler. 135 | """ 136 | logger.warning("You should implement and register you own on_created_event handler " 137 | "to receive the opt status for an Event that you sent to the VEN. This " 138 | "handler will receive a ven_id, event_id and opt_status. " 139 | "You don't need to return anything from this handler.") 140 | return None 141 | -------------------------------------------------------------------------------- /openleadr/service/opt_service.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from . import service, handler, VTNService 18 | import logging 19 | logger = logging.getLogger('openleadr') 20 | 21 | # ╔══════════════════════════════════════════════════════════════════════════╗ 22 | # ║ OPT SERVICE ║ 23 | # ╚══════════════════════════════════════════════════════════════════════════╝ 24 | # ┌──────────────────────────────────────────────────────────────────────────┐ 25 | # │ The VEN can send an Opt-in / Opt-out schedule to the VTN: │ 26 | # │ │ 27 | # │ ┌────┐ ┌────┐ │ 28 | # │ │VEN │ │VTN │ │ 29 | # │ └─┬──┘ └─┬──┘ │ 30 | # │ │───────────────────────────oadrCreateOpt()──────────────────────▶│ │ 31 | # │ │ │ │ 32 | # │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ oadrCreatedOpt()─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ 33 | # │ │ │ │ 34 | # │ │ 35 | # └──────────────────────────────────────────────────────────────────────────┘ 36 | # ┌──────────────────────────────────────────────────────────────────────────┐ 37 | # │ The VEN can cancel a sent Opt-in / Opt-out schedule: │ 38 | # │ │ 39 | # │ ┌────┐ ┌────┐ │ 40 | # │ │VEN │ │VTN │ │ 41 | # │ └─┬──┘ └─┬──┘ │ 42 | # │ │───────────────────────────oadrCancelOpt()──────────────────────▶│ │ 43 | # │ │ │ │ 44 | # │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ oadrCanceledOpt()─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ 45 | # │ │ │ │ 46 | # │ │ 47 | # └──────────────────────────────────────────────────────────────────────────┘ 48 | 49 | 50 | @service('EiOpt') 51 | class OptService(VTNService): 52 | 53 | def __init__(self, vtn_id): 54 | super().__init__(vtn_id) 55 | self.created_opt_schedules = {} 56 | 57 | @handler('oadrCreateOpt') 58 | async def create_opt(self, payload): 59 | """ 60 | Handle an opt schedule created by the VEN 61 | """ 62 | 63 | pass # TODO: call handler and return the result (oadrCreatedOpt) 64 | 65 | def on_create_opt(self, payload): 66 | """ 67 | Implementation of the on_create_opt handler, may be overwritten by the user. 68 | """ 69 | ven_id = payload['ven_id'] 70 | 71 | if payload['ven_id'] not in self.created_opt_schedules: 72 | self.created_opt_schedules[ven_id] = [] 73 | 74 | # TODO: internally create an opt schedule and save it, if this is an optional handler then make sure to handle None returns 75 | 76 | return 'oadrCreatedOpt', {'opt_id': payload['opt_id']} 77 | 78 | @handler('oadrCancelOpt') 79 | async def cancel_opt(self, payload): 80 | """ 81 | Cancel an opt schedule previously created by the VEN 82 | """ 83 | ven_id = payload['ven_id'] 84 | opt_id = payload['opt_id'] 85 | 86 | pass # TODO: call handler and return result (oadrCanceledOpt) 87 | 88 | def on_cancel_opt(self, ven_id, opt_id): 89 | """ 90 | Placeholder for the on_cancel_opt handler. 91 | """ 92 | 93 | # TODO: implement cancellation of previously acknowledged opt schedule, if this is an optional handler make sure to hande None returns 94 | 95 | return 'oadrCanceledOpt', {'opt_id': opt_id} 96 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCancelOpt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | {{ opt_id }} 5 | {{ ven_id }} 6 | 7 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCancelPartyRegistration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | {{ registration_id }} 5 | {{ ven_id }} 6 | 7 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCancelReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | {{ report_request_id }} 5 | {{ report_to_follow|booleanformat }} 6 | {{ ven_id }} 7 | 8 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCanceledOpt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {{ response.request_id }} 7 | 8 | {{ opt_id }} 9 | 10 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCanceledPartyRegistration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {{ response.request_id }} 7 | 8 | {{ registration_id }} 9 | {{ ven_id }} 10 | 11 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCanceledReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {{ response.request_id }} 7 | 8 | 9 | {% for pending_report in pending_reports %} 10 | {{ pending_report.report_request_id }} 11 | {% endfor %} 12 | 13 | {% if ven_id is defined and ven_id is not none %} 14 | {{ ven_id }} 15 | {% endif %} 16 | 17 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCreateOpt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ opt_id }} 4 | {{ opt_type }} 5 | {{ opt_reason }} 6 | {% if market_context is defined and market_context is not none %} 7 | {{ market_context }} 8 | {% endif %} 9 | {{ ven_id }} 10 | {% if vavailability is defined and vavailability is not none %} 11 | 12 | 13 | {% for component in vavailability.components %} 14 | 15 | 16 | 17 | {{ component.dtstart|datetimeformat }} 18 | 19 | 20 | {{ component.duration|timedeltaformat }} 21 | 22 | 23 | {% endfor %} 24 | 25 | {% endif %} 26 | {{ created_date_time|datetimeformat }} 27 | {{ request_id }} 28 | {% if event_id is defined and event_id is not none %} 29 | 30 | {{ event_id }} 31 | {{ modification_number }} 32 | 33 | {% endif %} 34 | {% if targets is defined and targets is not none and targets|length > 0 %} 35 | {% for target in targets %} 36 | {% include 'parts/eiTarget.xml' %} 37 | {% endfor %} 38 | {% else %} 39 | 40 | {% endif %} 41 | {% if signal_target_mrid is defined and signal_target_mrid is not none %} 42 | 43 | 44 | {{ signal_target_mrid }} 45 | 46 | 47 | {% endif %} 48 | 49 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCreatePartyRegistration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | {% if registration_id is defined and registration_id is not none %} 5 | {{ registration_id }} 6 | {% endif %} 7 | {% if ven_id is defined and ven_id is not none %} 8 | {{ ven_id }} 9 | {% endif %} 10 | {{ profile_name }} 11 | {{ transport_name }} 12 | {{ transport_address }} 13 | {{ report_only|booleanformat }} 14 | {{ xml_signature|booleanformat }} 15 | {{ ven_name }} 16 | {{ http_pull_model|booleanformat }} 17 | 18 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCreateReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | {% for report_request in report_requests %} 5 | {% include 'parts/oadrReportRequest.xml' %} 6 | {% endfor %} 7 | {{ ven_id }} 8 | 9 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCreatedEvent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ response.response_code }} 6 | {{ response.response_description }} 7 | {% if (event_responses is not defined or event_responses is none) and (response.request_id is defined and response.request_id is not none) %} 8 | {{ response.request_id }} 9 | {% else %} 10 | 11 | {% endif %} 12 | 13 | {% if event_responses is defined and event_responses is not none %} 14 | 15 | {% for event_response in event_responses %} 16 | 17 | {{ event_response.response_code }} 18 | {% if event_response.response_description is defined and event_response.response_description is not none %} 19 | {{ event_response.response_description }} 20 | {% endif %} 21 | {{ event_response.request_id }} 22 | 23 | {{ event_response.event_id }} 24 | {{ event_response.modification_number }} 25 | 26 | {{ event_response.opt_type }} 27 | 28 | {% endfor %} 29 | 30 | {% endif %} 31 | {{ ven_id }} 32 | 33 | 34 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCreatedOpt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {{ response.request_id }} 7 | 8 | {{ opt_id }} 9 | 10 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCreatedPartyRegistration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {{ response.request_id }} 7 | 8 | {% if registration_id is defined and registration_id is not none %} 9 | {{ registration_id }} 10 | {% else %} 11 | 12 | {% endif %} 13 | 14 | {% if ven_id is defined and ven_id is not none %} 15 | {{ ven_id }} 16 | {% else %} 17 | 18 | {% endif %} 19 | 20 | {{ vtn_id }} 21 | 22 | {% for profile in profiles %} 23 | 24 | {{ profile.profile_name }} 25 | 26 | {% for transport in profile.transports %} 27 | 28 | {{ transport.transport_name }} 29 | 30 | {% endfor %} 31 | 32 | 33 | {% endfor %} 34 | 35 | {% if requested_oadr_poll_freq is defined and requested_oadr_poll_freq is not none %} 36 | 37 | {{ requested_oadr_poll_freq|timedeltaformat }} 38 | 39 | {% endif %} 40 | 41 | 42 | -------------------------------------------------------------------------------- /openleadr/templates/oadrCreatedReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {{ response.request_id }} 7 | 8 | 9 | {% for pending_report in pending_reports %} 10 | {{ pending_report.report_request_id }} 11 | {% endfor %} 12 | 13 | {% if ven_id is defined and ven_id is not none %} 14 | {{ ven_id }} 15 | {% endif %} 16 | 17 | -------------------------------------------------------------------------------- /openleadr/templates/oadrDistributeEvent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if response is defined and response is not none %} 4 | 5 | {{ response.response_code }} 6 | {{ response.response_description }} 7 | {{ response.request_id }} 8 | 9 | {% endif %} 10 | {{ request_id }} 11 | {{ vtn_id }} 12 | {% for event in events %} 13 | {% include 'parts/eiEvent.xml' %} 14 | {% endfor %} 15 | 16 | -------------------------------------------------------------------------------- /openleadr/templates/oadrPayload.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if signature: %}{{ signature|safe }}{% endif %} 4 | {{ signed_object|safe }} 5 | -------------------------------------------------------------------------------- /openleadr/templates/oadrPoll.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ ven_id }} 4 | 5 | -------------------------------------------------------------------------------- /openleadr/templates/oadrQueryRegistration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | 5 | -------------------------------------------------------------------------------- /openleadr/templates/oadrRegisterReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | {% for report in reports %} 5 | 6 | {% if report.duration is defined and report.duration is not none %} 7 | 8 | {{ report.duration|timedeltaformat }} 9 | 10 | {% endif %} 11 | {% if report.report_id is defined and report.report_id is not none and not report.report_name.startswith('METADATA') %} 12 | {{ report.report_id }} 13 | {% endif %} 14 | {% for report_description in report.report_descriptions %} 15 | {% include 'parts/oadrReportDescription.xml' %} 16 | {% endfor %} 17 | {% if report.report_request_id is defined and report.report_request_id is not none %} 18 | {{ report.report_request_id }} 19 | {% else %} 20 | 0 21 | {% endif %} 22 | {{ report.report_specifier_id }} 23 | {{ report.report_name }} 24 | {{ report.created_date_time|datetimeformat }} 25 | 26 | {% endfor %} 27 | {% if ven_id is defined and ven_id is not none %} 28 | {{ ven_id }} 29 | {% endif %} 30 | 31 | -------------------------------------------------------------------------------- /openleadr/templates/oadrRegisteredReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {{ response.request_id }} 7 | 8 | {% for report_request in report_requests %} 9 | {% include 'parts/oadrReportRequest.xml' %} 10 | {% endfor %} 11 | {% if ven_id is defined and ven_id is not none %} 12 | {{ ven_id }} 13 | {% endif %} 14 | 15 | -------------------------------------------------------------------------------- /openleadr/templates/oadrRequestEvent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ request_id }} 5 | {{ ven_id }} 6 | {% if reply_limit is defined and reply_limit is not none %} 7 | {{ reply_limit }} 8 | {% endif %} 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /openleadr/templates/oadrRequestReregistration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ ven_id }} 4 | 5 | -------------------------------------------------------------------------------- /openleadr/templates/oadrResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {% if response.request_id is defined and response.request_id is not none %} 7 | {{ response.request_id }} 8 | {% else %} 9 | 10 | {% endif %} 11 | 12 | {{ ven_id }} 13 | 14 | -------------------------------------------------------------------------------- /openleadr/templates/oadrUpdateReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ request_id }} 4 | {% if reports %} 5 | {% for report in reports %} 6 | 7 | {% if report.dtstart is defined and report.dtstart is not none %} 8 | 9 | {{ report.dtstart|datetimeformat }} 10 | 11 | {% endif %} 12 | 13 | {% if report.intervals %} 14 | 15 | {% for interval in report.intervals %} 16 | 17 | 18 | {{ interval.dtstart|datetimeformat }} 19 | 20 | {% if interval.duration is defined and interval.duration is not none %} 21 | 22 | {{ interval.duration|timedeltaformat }} 23 | 24 | {% endif %} 25 | 26 | {{ interval.report_payload.r_id }} 27 | {% if interval.report_payload.confidence is defined and interval.report_payload.confidence is not none %} 28 | {{ interval.report_payload.confidence }} 29 | {% endif %} 30 | {% if interval.report_payload.accuracy is defined and interval.report_payload.accuracy is not none %} 31 | {{ interval.report_payload.accuracy }} 32 | {% endif %} 33 | 34 | {{ interval.report_payload.value }} 35 | 36 | {% if interval.report_payload.data_quality is defined and interval.report_payload.data_quality is not none %} 37 | {{ interval.report_payload.data_quality }} 38 | {% endif %} 39 | 40 | 41 | {% endfor %} 42 | 43 | {% endif %} 44 | 45 | {{ report.report_id }} 46 | {% if report.report_descriptions %} 47 | {% for report_description in report.report_descriptions %} 48 | {% include 'parts/oadrReportDescription.xml' %} 49 | {% endfor %} 50 | {% endif %} 51 | {{ report.report_request_id }} 52 | {{ report.report_specifier_id }} 53 | {% if report.report_name %} 54 | {{ report.report_name }} 55 | {% endif %} 56 | {{ report.created_date_time|datetimeformat }} 57 | 58 | {% endfor %} 59 | {% endif %} 60 | {% if ven_id is defined and ven_id is not none %} 61 | {{ ven_id }} 62 | {% endif %} 63 | 64 | 65 | -------------------------------------------------------------------------------- /openleadr/templates/oadrUpdatedReport.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ response.response_code }} 5 | {{ response.response_description }} 6 | {% if response.request_id is defined and response.request_id is not none %} 7 | {{ response.request_id }} 8 | {% else %} 9 | 10 | {% endif %} 11 | 12 | {% if cancel_report is defined and cancel_report is not none %} 13 | 14 | {{ cancel_report.request_id }} 15 | {% for report_request_id in cancel_report.report_request_id %} 16 | {{ report_request_id }} 17 | {% endfor %} 18 | {{ cancel_report.report_to_follow|booleanformat }} 19 | {% if cancel_report.ven_id %} 20 | {{ cancel_report.ven_id }} 21 | {% endif %} 22 | 23 | {% endif %} 24 | {% if ven_id is defined and ven_id is not none %} 25 | {{ ven_id }} 26 | {% endif %} 27 | 28 | -------------------------------------------------------------------------------- /openleadr/templates/parts/eiActivePeriod.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ event.active_period.dtstart|datetimeformat }} 5 | 6 | 7 | {{ event.active_period.duration|timedeltaformat }} 8 | 9 | {% if event.active_period.tolerance %} 10 | 11 | 12 | {{ event.active_period.tolerance.startafter|timedeltaformat }} 13 | 14 | 15 | {% endif %} 16 | {% if event.active_period.notification_period %} 17 | 18 | {{ event.active_period.notification_period|timedeltaformat }} 19 | 20 | {% endif %} 21 | {% if event.active_period.ramp_up_period %} 22 | 23 | {{ event.active_period.ramp_up_period|timedeltaformat }} 24 | 25 | {% endif %} 26 | {% if event.active_period.recovery_period %} 27 | 28 | {{ event.active_period.recovery_period|timedeltaformat }} 29 | 30 | {% endif %} 31 | 32 | 33 | -------------------------------------------------------------------------------- /openleadr/templates/parts/eiEvent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include 'parts/eiEventDescriptor.xml' %} 4 | {% include 'parts/eiActivePeriod.xml' %} 5 | 6 | {% for event_signal in event.event_signals %} 7 | {% include 'parts/eiEventSignal.xml' %} 8 | {% endfor %} 9 | 10 | {% if event.report_requests %} 11 | 12 | {% for report_request in event.report_requests %} 13 | {% include 'parts/eiReportRequest.xml' %} 14 | {% endfor %} 15 | 16 | {% endif %} 17 | {% if event.targets is defined and event.targets is not none %} 18 | {% include 'parts/eiEventTarget.xml' %} 19 | {% endif %} 20 | 21 | {{ event.response_required }} 22 | -------------------------------------------------------------------------------- /openleadr/templates/parts/eiEventDescriptor.xml: -------------------------------------------------------------------------------- 1 | 2 | {{ event.event_descriptor.event_id }} 3 | {{ event.event_descriptor.modification_number }} 4 | {% if event.event_descriptor.modification_date_time is defined and event.event_descriptor.modification_date_time is not none %} 5 | {{ event.event_descriptor.modification_date_time|datetimeformat }} 6 | {% endif %} 7 | {% if event.event_descriptor.modification_reason is defined and event.event_descriptor.modification_reason is not none %} 8 | {{ event.event_descriptor.modification_reason }} 9 | {% endif %} 10 | {%if event.event_descriptor.priority is not none %} 11 | {{ event.event_descriptor.priority }} 12 | {% endif %} 13 | 14 | {{ event.event_descriptor.market_context }} 15 | 16 | {{ event.event_descriptor.created_date_time|datetimeformat }} 17 | {{ event.event_descriptor.event_status }} 18 | {% if event.event_descriptor.test_event is defined and event.event_descriptor.test_event is not none %} 19 | {{ event.event_descriptor.test_event|booleanformat }} 20 | {% endif %} 21 | {% if event.event_descriptor.vtn_comment is defined and event.event_descriptor.vtn_comment is not none %} 22 | {{ event.event_descriptor.vtn_comment }} 23 | {% endif %} 24 | -------------------------------------------------------------------------------- /openleadr/templates/parts/eiEventSignal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for interval in event_signal.intervals %} 4 | 5 | {% if interval.dtstart is defined and interval.dtstart is not none %} 6 | 7 | {{ interval.dtstart|datetimeformat }} 8 | 9 | {% endif %} 10 | {% if interval.duration is defined and interval.duration is not none %} 11 | 12 | {{ interval.duration|timedeltaformat }} 13 | 14 | {% endif %} 15 | 16 | {{ loop.index0 }} 17 | 18 | 19 | 20 | {{ interval.signal_payload }} 21 | 22 | 23 | 24 | {% endfor %} 25 | 26 | {{ event_signal.signal_name }} 27 | {{ event_signal.signal_type }} 28 | {{ event_signal.signal_id }} 29 | {% include 'parts/eventSignalEmix.xml' %} 30 | {% if event_signal.current_value is defined and event_signal.current_value is not none %} 31 | 32 | 33 | {{ event_signal.current_value }} 34 | 35 | 36 | {% endif %} 37 | -------------------------------------------------------------------------------- /openleadr/templates/parts/eiEventTarget.xml: -------------------------------------------------------------------------------- 1 | 2 | {% for target in event.targets %} 3 | {% if target.emix_interfaces %} 4 | {% for emix_interface in target.emix_interface %} 5 | {% include 'parts/emixInterface.xml' %} 6 | {% endfor %} 7 | {% endif %} 8 | {% endfor %} 9 | 10 | {% for target in event.targets %} 11 | {% if target.group_id %} 12 | {{ target.group_id }} 13 | {% endif %} 14 | {% endfor %} 15 | 16 | {% for target in event.targets %} 17 | {% if target.group_name %} 18 | {{ target.group_name }} 19 | {% endif %} 20 | {% endfor %} 21 | 22 | {% for target in event.targets %} 23 | {% if target.resource_id %} 24 | {{ target.resource_id }} 25 | {% endif %} 26 | {% endfor %} 27 | 28 | {% for target in event.targets %} 29 | {% if target.ven_id %} 30 | {{ target.ven_id }} 31 | {% endif %} 32 | {% endfor %} 33 | 34 | {% for target in event.targets %} 35 | {% if target.party_id %} 36 | {{ target.party_id }} 37 | {% endif %} 38 | {% endfor %} 39 | -------------------------------------------------------------------------------- /openleadr/templates/parts/eiTarget.xml: -------------------------------------------------------------------------------- 1 | 2 | {% if target.emix_interfaces is defined and target.emix_interfaces is not none %} 3 | {% for emix_interface in target.emix_interface %} 4 | {% include 'parts/emixInterface.xml' %} 5 | {% endfor %} 6 | {% endif %} 7 | 8 | {% if target.group_id is defined and target.group_id is not none %} 9 | {{ target.group_id }} 10 | {% endif %} 11 | 12 | {% if target.group_name is defined and target.group_name is not none %} 13 | {{ target.group_name }} 14 | {% endif %} 15 | 16 | {% if target.resource_id is defined and target.resource_id is not none %} 17 | {{ target.resource_id }} 18 | {% endif %} 19 | 20 | {% if target.ven_id is defined and target.ven_id is not none %} 21 | {{ target.ven_id }} 22 | {% endif %} 23 | 24 | {% if target.party_id is defined and target.party_id is not none %} 25 | {{ target.party_id }} 26 | {% endif %} 27 | -------------------------------------------------------------------------------- /openleadr/templates/parts/emixInterface.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /openleadr/templates/parts/eventSignalEmix.xml: -------------------------------------------------------------------------------- 1 | {% if event_signal.measurement is defined and event_signal.measurement is not none %} 2 | <{{ event_signal.measurement.ns }}:{{ event_signal.measurement.name }} xmlns:scale="http://docs.oasis-open.org/ns/emix/2011/06/siscale" xmlns:power="http://docs.oasis-open.org/ns/emix/2011/06/power" > 3 | <{{ event_signal.measurement.ns }}:itemDescription>{{ event_signal.measurement.description }} 4 | <{{ event_signal.measurement.ns }}:itemUnits>{{ event_signal.measurement.unit }} 5 | {% if event_signal.measurement.pulse_factor %}{{ event_signal.measurement.pulse_factor }}{% else %} 6 | {{ event_signal.measurement.scale }} 7 | {% endif %} 8 | {% if event_signal.measurement.power_attributes %} 9 | 10 | {{ event_signal.measurement.power_attributes.hertz }} 11 | {{ event_signal.measurement.power_attributes.voltage }} 12 | {{ event_signal.measurement.power_attributes.ac|booleanformat }} 13 | 14 | {% endif %} 15 | 16 | {% endif %} -------------------------------------------------------------------------------- /openleadr/templates/parts/oadrReportDescription.xml: -------------------------------------------------------------------------------- 1 | 2 | {{ report_description.r_id }} 3 | {% if report_description.report_subject and report_description.report_subject.end_device_asset.mrid %} 4 | 5 | 6 | {{ report_description.report_subject.end_device_asset.mrid }} 7 | 8 | 9 | {% endif %} 10 | 11 | {% if report_description.report_data_source %} 12 | 13 | {% if report_description.report_data_source.emix_interfaces %} 14 | {% for emix_interface in target.emix_interface %} 15 | {% include 'parts/emixInterface.xml' %} 16 | {% endfor %} 17 | {% endif %} 18 | 19 | {% if report_description.report_data_source.group_id %} 20 | {{ report_description.report_data_source.group_id }} 21 | {% endif %} 22 | 23 | {% if report_description.report_data_source.group_name %} 24 | {{ report_description.report_data_source.group_name }} 25 | {% endif %} 26 | 27 | {% if report_description.report_data_source.resource_id %} 28 | {{ report_description.report_data_source.resource_id }} 29 | {% endif %} 30 | 31 | {% if report_description.report_data_source.ven_id %} 32 | {{ report_description.report_data_source.ven_id }} 33 | {% endif %} 34 | 35 | {% if report_description.report_data_source.party_id %} 36 | {{ report_description.report_data_source.party_id }} 37 | {% endif %} 38 | 39 | {% endif %} 40 | {{ report_description.report_type }} 41 | {% include 'parts/reportDescriptionEmix.xml' %} 42 | {{ report_description.reading_type }} 43 | {% if report_description.market_context %} 44 | {{ report_description.market_context }} 45 | {% endif %} 46 | {% if report_description.sampling_rate %} 47 | 48 | {{ report_description.sampling_rate.min_period|timedeltaformat }} 49 | {{ report_description.sampling_rate.max_period|timedeltaformat }} 50 | {{ report_description.sampling_rate.on_change|booleanformat }} 51 | 52 | {% endif %} 53 | 54 | -------------------------------------------------------------------------------- /openleadr/templates/parts/oadrReportRequest.xml: -------------------------------------------------------------------------------- 1 | 2 | {{ report_request.report_request_id }} 3 | 4 | {{ report_request.report_specifier.report_specifier_id }} 5 | 6 | {{ report_request.report_specifier.granularity|timedeltaformat }} 7 | 8 | {% if report_request.report_specifier.report_back_duration is defined and report_request.report_specifier.report_back_duration is not none %} 9 | 10 | {{ report_request.report_specifier.report_back_duration|timedeltaformat }} 11 | 12 | {% endif %} 13 | {% if report_request.report_specifier.report_interval is defined and report_request.report_specifier.report_interval is not none %} 14 | 15 | 16 | 17 | {{ report_request.report_specifier.report_interval.dtstart|datetimeformat }} 18 | 19 | 20 | {{ report_request.report_specifier.report_interval.duration|timedeltaformat }} 21 | 22 | 23 | 24 | {% endif %} 25 | {% for specifier_payload in report_request.report_specifier.specifier_payloads %} 26 | 27 | {{ specifier_payload.r_id }} 28 | {{ specifier_payload.reading_type }} 29 | 30 | {% endfor %} 31 | 32 | -------------------------------------------------------------------------------- /openleadr/templates/parts/reportDescriptionEmix.xml: -------------------------------------------------------------------------------- 1 | {% if report_description.measurement is defined and report_description.measurement is not none %} 2 | <{{ report_description.measurement.ns }}:{{ report_description.measurement.name }} xmlns:scale="http://docs.oasis-open.org/ns/emix/2011/06/siscale" xmlns:power="http://docs.oasis-open.org/ns/emix/2011/06/power" > 3 | <{{ report_description.measurement.ns }}:itemDescription>{{ report_description.measurement.description }} 4 | <{{ report_description.measurement.ns }}:itemUnits>{{ report_description.measurement.unit }} 5 | {% if report_description.measurement.pulse_factor %}{{ report_description.measurement.pulse_factor }}{% else %} 6 | {{ report_description.measurement.scale }} 7 | {% endif %} 8 | {% if report_description.measurement.power_attributes %} 9 | 10 | {{ report_description.measurement.power_attributes.hertz }} 11 | {{ report_description.measurement.power_attributes.voltage }} 12 | {{ report_description.measurement.power_attributes.ac|booleanformat }} 13 | 14 | {% endif %} 15 | 16 | {% endif %} -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode = strict -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from setuptools import setup 18 | 19 | with open('README.md', 'r', encoding='utf-8') as file: 20 | long_description = file.read() 21 | 22 | setup(name='openleadr', 23 | version='0.5.34', 24 | description='Python3 library for building OpenADR Clients (VENs) and Servers (VTNs)', 25 | long_description=long_description, 26 | long_description_content_type='text/markdown', 27 | url='https://openleadr.org', 28 | project_urls={'GitHub': 'https://github.com/openleadr/openleadr-python', 29 | 'Documentation': 'https://openleadr.org/docs'}, 30 | packages=['openleadr', 'openleadr.service'], 31 | python_requires='>=3.7.0', 32 | include_package_data=True, 33 | install_requires=['xmltodict==0.13.0', 'aiohttp>=3.8.3,<4.0.0', 'apscheduler>=3.10.0,<4.0.0', 'jinja2>=3.1.2,<4.0.0', 'signxml==3.2.1'], 34 | entry_points={'console_scripts': ['fingerprint = openleadr.fingerprint:show_fingerprint']}) 35 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | -------------------------------------------------------------------------------- /test/conformance/test_conformance_001.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from openleadr import OpenADRClient, OpenADRServer, enums 20 | from openleadr.utils import generate_id 21 | from openleadr.messaging import create_message, parse_message 22 | from datetime import datetime, timezone, timedelta 23 | 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_conformance_001(): 28 | dt = datetime(2020,1,1,12,0,0,tzinfo=timezone(offset=timedelta(hours=4))) 29 | msg = create_message('oadrCreateOpt', **{'opt_id': generate_id(), 30 | 'opt_type': enums.OPT.OPT_IN, 31 | 'opt_reason': enums.OPT_REASON.ECONOMIC, 32 | 'ven_id': generate_id(), 33 | 'created_date_time': dt, 34 | 'request_id': generate_id(), 35 | 'event_id': generate_id(), 36 | 'modification_number': 1, 37 | 'targets': [{'ven_id': '123'}]}) 38 | parsed_type, parsed_msg = parse_message(msg) 39 | assert parsed_msg['created_date_time'].tzinfo == timezone.utc 40 | assert parsed_msg['created_date_time'] == dt.astimezone(timezone.utc) 41 | 42 | -------------------------------------------------------------------------------- /test/conformance/test_conformance_002.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from openleadr import OpenADRClient, OpenADRServer, enums 20 | from openleadr.utils import generate_id 21 | from openleadr.messaging import create_message, parse_message 22 | from datetime import datetime, timezone, timedelta 23 | 24 | from pprint import pprint 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_conformance_002(): 29 | """ 30 | The uid element is REQUIRED for each eiEventSignal interval. Within a sin- 31 | gle oadrDistributeEvent eiEventSignal, uid MUST be expressed as an inter- 32 | val number with a base of 0 and an increment of 1 for each subsequent in- 33 | terval. 34 | """ 35 | event_id = generate_id() 36 | event = {'event_descriptor': 37 | {'event_id': event_id, 38 | 'modification_number': 0, 39 | 'modification_date': datetime.now(), 40 | 'priority': 0, 41 | 'market_context': 'MarketContext001', 42 | 'created_date_time': datetime.now(), 43 | 'event_status': enums.EVENT_STATUS.FAR, 44 | 'test_event': False, 45 | 'vtn_comment': 'No Comment'}, 46 | 'active_period': 47 | {'dtstart': datetime.now(), 48 | 'duration': timedelta(minutes=30)}, 49 | 'event_signals': 50 | [{'intervals': [{'duration': timedelta(minutes=10), 51 | 'signal_payload': 1}, 52 | {'duration': timedelta(minutes=10), 53 | 'signal_payload': 2}, 54 | {'duration': timedelta(minutes=10), 55 | 'signal_payload': 3}], 56 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 57 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 58 | 'signal_id': generate_id() 59 | }], 60 | 'targets': [{'ven_id': '123'}] 61 | } 62 | 63 | # Create a message with this event 64 | msg = create_message('oadrDistributeEvent', 65 | response={'response_code': 200, 66 | 'response_description': 'OK', 67 | 'request_id': generate_id()}, 68 | request_id=generate_id(), 69 | vtn_id=generate_id(), 70 | events=[event]) 71 | 72 | # Parse the message 73 | parsed_type, parsed_msg = parse_message(msg) 74 | assert parsed_type == 'oadrDistributeEvent' 75 | intervals = parsed_msg['events'][0]['event_signals'][0]['intervals'] 76 | 77 | # Verify that the interval uid's are numbered consecutively and starting at 0 78 | assert intervals[0]['uid'] == 0 79 | assert intervals[0]['signal_payload'] == 1 80 | assert intervals[1]['uid'] == 1 81 | assert intervals[1]['signal_payload'] == 2 82 | assert intervals[2]['uid'] == 2 83 | assert intervals[2]['signal_payload'] == 3 84 | -------------------------------------------------------------------------------- /test/conformance/test_conformance_006.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from openleadr import OpenADRClient, OpenADRServer, enums 20 | from openleadr.utils import generate_id 21 | from openleadr.messaging import create_message, parse_message 22 | from datetime import datetime, timezone, timedelta 23 | 24 | from pprint import pprint 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_conformance_006(): 29 | """ 30 | The presence of any string except “false” in the oadrDistributeEvent 31 | testEvent element MUST be treated as a trigger for a test event. 32 | """ 33 | 34 | # Monkey patch our own formatter to prevent an error being raised 35 | from openleadr.messaging import TEMPLATES 36 | def booleanformat_monkey(value): 37 | """ 38 | Format a boolean value 39 | """ 40 | if isinstance(value, bool): 41 | if value == True: 42 | return "true" 43 | elif value == False: 44 | return "false" 45 | else: 46 | return value 47 | 48 | booleanformat_original = TEMPLATES.filters['booleanformat'] 49 | TEMPLATES.filters['booleanformat'] = booleanformat_monkey 50 | 51 | event_id = generate_id() 52 | event = {'event_descriptor': 53 | {'event_id': event_id, 54 | 'modification_number': 0, 55 | 'modification_date': datetime.now(), 56 | 'priority': 0, 57 | 'market_context': 'MarketContext001', 58 | 'created_date_time': datetime.now(), 59 | 'event_status': enums.EVENT_STATUS.FAR, 60 | 'test_event': "HelloThere", 61 | 'vtn_comment': 'No Comment'}, 62 | 'active_period': 63 | {'dtstart': datetime.now(), 64 | 'duration': timedelta(minutes=30)}, 65 | 'event_signals': 66 | [{'intervals': [{'duration': timedelta(minutes=10), 67 | 'signal_payload': 1}, 68 | {'duration': timedelta(minutes=10), 69 | 'signal_payload': 2}, 70 | {'duration': timedelta(minutes=10), 71 | 'signal_payload': 3}], 72 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 73 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 74 | 'signal_id': generate_id() 75 | }], 76 | 'targets': [{'ven_id': '123'}] 77 | } 78 | 79 | # Create a message with this event 80 | msg = create_message('oadrDistributeEvent', 81 | response={'response_code': 200, 82 | 'response_description': 'OK', 83 | 'request_id': generate_id()}, 84 | request_id=generate_id(), 85 | vtn_id=generate_id(), 86 | events=[event]) 87 | 88 | parsed_type, parsed_message = parse_message(msg) 89 | assert parsed_type == 'oadrDistributeEvent' 90 | assert parsed_message['events'][0]['event_descriptor']['test_event'] == True 91 | 92 | # Restore the original booleanformat function 93 | TEMPLATES.filters['booleanformat'] = booleanformat_original 94 | -------------------------------------------------------------------------------- /test/conformance/test_conformance_008.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from openleadr import OpenADRClient, OpenADRServer, enums 20 | from openleadr.utils import generate_id 21 | from openleadr.messaging import create_message, parse_message 22 | from datetime import datetime, timezone, timedelta 23 | 24 | from pprint import pprint 25 | import logging 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_conformance_008_autocorrect(caplog): 30 | """ 31 | oadrDistributeEvent eventSignal interval durations for a given event MUST 32 | add up to eiEvent eiActivePeriod duration. 33 | """ 34 | event_id = generate_id() 35 | event = {'event_descriptor': 36 | {'event_id': event_id, 37 | 'modification_number': 0, 38 | 'modification_date': datetime.now(), 39 | 'priority': 0, 40 | 'market_context': 'MarketContext001', 41 | 'created_date_time': datetime.now(), 42 | 'event_status': enums.EVENT_STATUS.FAR, 43 | 'test_event': False, 44 | 'vtn_comment': 'No Comment'}, 45 | 'active_period': 46 | {'dtstart': datetime.now(), 47 | 'duration': timedelta(minutes=5)}, 48 | 'event_signals': 49 | [{'intervals': [{'duration': timedelta(minutes=10), 50 | 'signal_payload': 1}, 51 | {'duration': timedelta(minutes=10), 52 | 'signal_payload': 2}, 53 | {'duration': timedelta(minutes=10), 54 | 'signal_payload': 3}], 55 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 56 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 57 | 'signal_id': generate_id() 58 | }], 59 | 'targets': [{'ven_id': '123'}] 60 | } 61 | 62 | # Create a message with this event 63 | msg = create_message('oadrDistributeEvent', 64 | response={'response_code': 200, 65 | 'response_description': 'OK', 66 | 'request_id': generate_id()}, 67 | request_id=generate_id(), 68 | vtn_id=generate_id(), 69 | events=[event]) 70 | 71 | assert ("openleadr", logging.WARNING, f"The active_period duration for event {event_id} (0:05:00) differs from the sum of the interval's durations (0:30:00). The active_period duration has been adjusted to (0:30:00).") in caplog.record_tuples 72 | 73 | parsed_type, parsed_msg = parse_message(msg) 74 | assert parsed_type == 'oadrDistributeEvent' 75 | total_time = sum([i['duration'] for i in parsed_msg['events'][0]['event_signals'][0]['intervals']], 76 | timedelta(seconds=0)) 77 | assert parsed_msg['events'][0]['active_period']['duration'] == total_time 78 | 79 | @pytest.mark.asyncio 80 | async def test_conformance_008_raise(): 81 | """ 82 | oadrDistributeEvent eventSignal interval durations for a given event MUST 83 | add up to eiEvent eiActivePeriod duration. 84 | """ 85 | event_id = generate_id() 86 | event = {'event_descriptor': 87 | {'event_id': event_id, 88 | 'modification_number': 0, 89 | 'modification_date': datetime.now(), 90 | 'priority': 0, 91 | 'market_context': 'MarketContext001', 92 | 'created_date_time': datetime.now(), 93 | 'event_status': enums.EVENT_STATUS.FAR, 94 | 'test_event': False, 95 | 'vtn_comment': 'No Comment'}, 96 | 'active_period': 97 | {'dtstart': datetime.now(), 98 | 'duration': timedelta(minutes=5)}, 99 | 'event_signals': 100 | [{'intervals': [{'duration': timedelta(minutes=10), 101 | 'signal_payload': 1}, 102 | {'duration': timedelta(minutes=10), 103 | 'signal_payload': 2}, 104 | {'duration': timedelta(minutes=10), 105 | 'signal_payload': 3}], 106 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 107 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 108 | 'signal_id': generate_id() 109 | }, 110 | {'intervals': [{'duration': timedelta(minutes=1), 111 | 'signal_payload': 1}, 112 | {'duration': timedelta(minutes=2), 113 | 'signal_payload': 2}, 114 | {'duration': timedelta(minutes=2), 115 | 'signal_payload': 3}], 116 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 117 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 118 | 'signal_id': generate_id() 119 | }] 120 | } 121 | 122 | with pytest.raises(ValueError): 123 | msg = create_message('oadrDistributeEvent', 124 | response={'response_code': 200, 125 | 'response_description': 'OK', 126 | 'request_id': generate_id()}, 127 | request_id=generate_id(), 128 | vtn_id=generate_id(), 129 | events=[event]) -------------------------------------------------------------------------------- /test/conformance/test_conformance_009.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from openleadr import OpenADRClient, OpenADRServer, enums 20 | from openleadr.utils import generate_id 21 | from openleadr.messaging import create_message, parse_message 22 | from datetime import datetime, timezone, timedelta 23 | 24 | from pprint import pprint 25 | import warnings 26 | 27 | @pytest.mark.asyncio 28 | async def test_conformance_009_pass(): 29 | """ 30 | oadrDistributeEvent eiEventSignal’s with a signalName of “SIMPLE” MUST 31 | use signalPayload values of 0=normal; 1=moderate; 2=high; 3=special. 32 | """ 33 | event_id = generate_id() 34 | event = {'event_descriptor': 35 | {'event_id': event_id, 36 | 'modification_number': 0, 37 | 'modification_date': datetime.now(), 38 | 'priority': 0, 39 | 'market_context': 'MarketContext001', 40 | 'created_date_time': datetime.now(), 41 | 'event_status': enums.EVENT_STATUS.FAR, 42 | 'test_event': False, 43 | 'vtn_comment': 'No Comment'}, 44 | 'active_period': 45 | {'dtstart': datetime.now(), 46 | 'duration': timedelta(minutes=30)}, 47 | 'event_signals': 48 | [{'intervals': [{'duration': timedelta(minutes=10), 49 | 'signal_payload': 1}, 50 | {'duration': timedelta(minutes=10), 51 | 'signal_payload': 2}, 52 | {'duration': timedelta(minutes=10), 53 | 'signal_payload': 3}], 54 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 55 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 56 | 'signal_id': generate_id() 57 | }], 58 | 'targets': [{'ven_id': '123'}] 59 | } 60 | 61 | # Create a message with this event 62 | msg = create_message('oadrDistributeEvent', 63 | response={'response_code': 200, 64 | 'response_description': 'OK', 65 | 'request_id': generate_id()}, 66 | request_id=generate_id(), 67 | vtn_id=generate_id(), 68 | events=[event]) 69 | 70 | @pytest.mark.asyncio 71 | async def test_conformance_009_raise(): 72 | """ 73 | oadrDistributeEvent eiEventSignal’s with a signalName of “SIMPLE” MUST 74 | use signalPayload values of 0=normal; 1=moderate; 2=high; 3=special. 75 | """ 76 | event_id = generate_id() 77 | event = {'event_descriptor': 78 | {'event_id': event_id, 79 | 'modification_number': 0, 80 | 'modification_date': datetime.now(), 81 | 'priority': 0, 82 | 'market_context': 'MarketContext001', 83 | 'created_date_time': datetime.now(), 84 | 'event_status': enums.EVENT_STATUS.FAR, 85 | 'test_event': False, 86 | 'vtn_comment': 'No Comment'}, 87 | 'active_period': 88 | {'dtstart': datetime.now(), 89 | 'duration': timedelta(minutes=30)}, 90 | 'event_signals': 91 | [{'intervals': [{'duration': timedelta(minutes=10), 92 | 'signal_payload': 10}, 93 | {'duration': timedelta(minutes=10), 94 | 'signal_payload': 20}, 95 | {'duration': timedelta(minutes=10), 96 | 'signal_payload': 30}], 97 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 98 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 99 | 'signal_id': generate_id() 100 | }] 101 | } 102 | 103 | with pytest.raises(ValueError): 104 | msg = create_message('oadrDistributeEvent', 105 | response={'response_code': 200, 106 | 'response_description': 'OK', 107 | 'request_id': generate_id()}, 108 | request_id=generate_id(), 109 | vtn_id=generate_id(), 110 | events=[event]) -------------------------------------------------------------------------------- /test/conformance/test_conformance_014.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from openleadr import OpenADRClient, OpenADRServer, enums 20 | from openleadr.utils import generate_id 21 | from openleadr.messaging import create_message, parse_message 22 | from datetime import datetime, timezone, timedelta 23 | import logging 24 | from pprint import pprint 25 | 26 | @pytest.mark.asyncio 27 | async def test_conformance_014_warn(caplog): 28 | """ 29 | If currentValue is included in the payload, it MUST be set to 0 (normal) 30 | when the event status is not “active” for the SIMPLE signalName. 31 | """ 32 | event_id = generate_id() 33 | event = {'event_descriptor': 34 | {'event_id': event_id, 35 | 'modification_number': 0, 36 | 'modification_date': datetime.now(), 37 | 'priority': 0, 38 | 'market_context': 'MarketContext001', 39 | 'created_date_time': datetime.now(), 40 | 'event_status': enums.EVENT_STATUS.FAR, 41 | 'test_event': False, 42 | 'vtn_comment': 'No Comment'}, 43 | 'active_period': 44 | {'dtstart': datetime.now() + timedelta(minutes=30), 45 | 'duration': timedelta(minutes=30)}, 46 | 'event_signals': 47 | [{'intervals': [{'duration': timedelta(minutes=10), 48 | 'signal_payload': 1}, 49 | {'duration': timedelta(minutes=10), 50 | 'signal_payload': 2}, 51 | {'duration': timedelta(minutes=10), 52 | 'signal_payload': 3}], 53 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 54 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 55 | 'signal_id': generate_id(), 56 | 'current_value': 123 57 | }], 58 | 'targets': {'ven_id': '123'} 59 | } 60 | 61 | # Create a message with this event 62 | msg = create_message('oadrDistributeEvent', 63 | response={'response_code': 200, 64 | 'response_description': 'OK', 65 | 'request_id': generate_id()}, 66 | request_id=generate_id(), 67 | vtn_id=generate_id(), 68 | events=[event]) 69 | 70 | assert ("openleadr", logging.WARNING, "The current_value for a SIMPLE event that is not yet active must be 0. This will be corrected.") in caplog.record_tuples 71 | 72 | -------------------------------------------------------------------------------- /test/conformance/test_conformance_021.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from openleadr import OpenADRClient, OpenADRServer, enums 20 | from openleadr.utils import generate_id, datetimeformat, timedeltaformat, booleanformat 21 | from openleadr.messaging import create_message, parse_message 22 | from openleadr.objects import Event, EventDescriptor, ActivePeriod, EventSignal, Interval 23 | from datetime import datetime, timezone, timedelta 24 | 25 | import json 26 | import sqlite3 27 | from pprint import pprint 28 | import warnings 29 | 30 | VEN_NAME = 'myven' 31 | VTN_ID = "TestVTN" 32 | 33 | async def lookup_ven(ven_name=None, ven_id=None): 34 | """ 35 | Look up a ven by its name or ID 36 | """ 37 | return {'ven_id': '1234'} 38 | 39 | async def on_update_report(report, futures=None): 40 | if futures: 41 | futures.pop().set_result(True) 42 | pass 43 | 44 | async def on_register_report(report, futures=None): 45 | """ 46 | Deal with this report. 47 | """ 48 | if futures: 49 | futures.pop().set_result(True) 50 | granularity = min(*[rd['sampling_rate']['min_period'] for rd in report['report_descriptions']]) 51 | return (on_update_report, granularity, [rd['r_id'] for rd in report['report_descriptions']]) 52 | 53 | async def on_create_party_registration(ven_name, future=None): 54 | if future: 55 | future.set_result(True) 56 | ven_id = '1234' 57 | registration_id = 'abcd' 58 | return ven_id, registration_id 59 | 60 | class EventFormatter(json.JSONEncoder): 61 | def default(self, obj): 62 | if isinstance(obj, timedelta): 63 | return timedeltaformat(obj) 64 | if isinstance(obj, datetime): 65 | return datetimeformat(obj) 66 | if isinstance(obj, bool): 67 | return booleanformat(obj) 68 | return json.JSONEncoder.default(self, obj) 69 | 70 | DB = sqlite3.connect(":memory:") 71 | with DB: 72 | DB.execute("CREATE TABLE vens (ven_id STRING, ven_name STRING, online BOOLEAN, last_seen DATETIME, registration_id STRING)") 73 | DB.execute("CREATE TABLE events (event_id STRING, ven_id STRING, request_id STRING, status STRING, event JSON, created_at DATETIME, updated_at DATETIME)") 74 | 75 | def add_ven(ven_name, ven_id, registration_id): 76 | with DB: 77 | DB.execute("""INSERT INTO vens (ven_id, ven_name, online, last_seen, registration_id) 78 | VALUES (?, ?, ?, ?, ?)""", (ven_id, ven_name, True, datetime.now().replace(microsecond=0), registration_id)) 79 | 80 | def add_event(ven_id, event_id, event): 81 | serialized_event = json.dumps(event, cls=EventFormatter) 82 | with DB: 83 | DB.execute("""INSERT INTO events (ven_id, event_id, request_id, status, event) 84 | VALUES (?, ?, ?, ?, ?)""", (ven_id, event_id, None, 'new', serialized_event)) 85 | 86 | async def _on_poll(ven_id, request_id=None): 87 | cur = DB.cursor() 88 | cur.execute("""SELECT event_id, event FROM events WHERE ven_id = ? AND status = 'new' LIMIT 1""", (ven_id,)) 89 | result = cur.fetchone() 90 | if result: 91 | event_id, event = result 92 | event_request_id = generate_id() 93 | with DB: 94 | DB.execute("""UPDATE events SET request_id = ? WHERE event_id = ?""", (event_request_id, event_id)) 95 | response_type = 'oadrDistributeEvent' 96 | response_payload = {'response': {'request_id': request_id, 97 | 'response_code': 200, 98 | 'response_description': 'OK'}, 99 | 'request_id': event_request_id, 100 | 'vtn_id': VTN_ID, 101 | 'events': [json.loads(event)]} 102 | else: 103 | response_type = 'oadrResponse' 104 | response_payload = {'response': {'request_id': request_id, 105 | 'response_code': 200, 106 | 'response_description': 'OK'}, 107 | 'ven_id': ven_id} 108 | return response_type, response_payload 109 | 110 | @pytest.mark.asyncio 111 | async def test_conformance_021(): 112 | """ 113 | If venID, vtnID, or eventID value is included in the payload, the receiving 114 | entity MUST validate that the ID value is as expected and generate an error 115 | if an unexpected value is received. 116 | Exception: A VEN MUST NOT generate an error upon receipt of a canceled 117 | event whose eventID is not previously known. 118 | """ 119 | server = OpenADRServer(vtn_id='TestVTN', http_port=8001) 120 | server.add_handler('on_create_party_registration', on_create_party_registration) 121 | server.add_handler('on_poll', _on_poll) 122 | await server.run_async() 123 | 124 | client = OpenADRClient(ven_name="TestVEN", 125 | vtn_url="http://localhost:8001/OpenADR2/Simple/2.0b") 126 | await client.create_party_registration() 127 | event = {'event_descriptor': 128 | {'event_id': generate_id(), 129 | 'modification_number': 0, 130 | 'modification_date': datetime.now(), 131 | 'priority': 0, 132 | 'market_context': 'MarketContext001', 133 | 'created_date_time': datetime.now(), 134 | 'event_status': enums.EVENT_STATUS.FAR, 135 | 'test_event': False, 136 | 'vtn_comment': 'No Comment'}, 137 | 'active_period': 138 | {'dtstart': datetime.now() + timedelta(minutes=30), 139 | 'duration': timedelta(minutes=30)}, 140 | 'event_signals': 141 | [{'intervals': [{'duration': timedelta(minutes=10), 142 | 'signal_payload': 1}, 143 | {'duration': timedelta(minutes=10), 144 | 'signal_payload': 2}, 145 | {'duration': timedelta(minutes=10), 146 | 'signal_payload': 3}], 147 | 'signal_name': enums.SIGNAL_NAME.SIMPLE, 148 | 'signal_type': enums.SIGNAL_TYPE.DELTA, 149 | 'signal_id': generate_id(), 150 | 'current_value': 123 151 | }], 152 | 'targets': [{'ven_id': '123'}] 153 | } 154 | add_event(ven_id=client.ven_id, 155 | event_id = event['event_descriptor']['event_id'], 156 | event=event) 157 | message_type, message_payload = await client.poll() 158 | assert message_type == 'oadrDistributeEvent' 159 | await client.stop() 160 | await server.stop() 161 | 162 | -------------------------------------------------------------------------------- /test/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenLEADR/openleadr-python/dd171272d91e5cecbc27ecc95e74919f02484260/test/fixtures/__init__.py -------------------------------------------------------------------------------- /test/fixtures/simple_server.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from openleadr import OpenADRClient, OpenADRServer, enums 18 | from openleadr.utils import generate_id, normalize_dict, timedeltaformat, datetimeformat, booleanformat 19 | from openleadr.messaging import create_message, parse_message 20 | from datetime import datetime, timezone, timedelta 21 | 22 | import asyncio 23 | import sqlite3 24 | import pytest 25 | import pytest_asyncio 26 | from aiohttp import web 27 | import json 28 | 29 | SERVER_PORT = 8001 30 | VEN_NAME = 'myven' 31 | VTN_ID = "TestVTN" 32 | 33 | class EventFormatter(json.JSONEncoder): 34 | def default(self, obj): 35 | if isinstance(obj, timedelta): 36 | return timedeltaformat(obj) 37 | if isinstance(obj, datetime): 38 | return datetimeformat(obj) 39 | if isinstance(obj, bool): 40 | return booleanformat(obj) 41 | return json.JSONEncoder.default(self, obj) 42 | 43 | DB = sqlite3.connect(":memory:") 44 | with DB: 45 | DB.execute("CREATE TABLE vens (ven_id STRING, ven_name STRING, online BOOLEAN, last_seen DATETIME, registration_id STRING)") 46 | DB.execute("CREATE TABLE events (event_id STRING, ven_id STRING, request_id STRING, status STRING, event JSON, created_at DATETIME, updated_at DATETIME)") 47 | 48 | def lookup_ven(ven_name): 49 | with DB: 50 | DB.execute("SELECT * FROM vens WHERE ven_name = ?", (ven_name,)) 51 | ven = DB.fetchone() 52 | return ven 53 | 54 | def add_ven(ven_name, ven_id, registration_id): 55 | with DB: 56 | DB.execute("""INSERT INTO vens (ven_id, ven_name, online, last_seen, registration_id) 57 | VALUES (?, ?, ?, ?, ?)""", (ven_id, ven_name, True, datetime.now().replace(microsecond=0), registration_id)) 58 | 59 | def add_event(ven_id, event_id, event): 60 | serialized_event = json.dumps(event, cls=EventFormatter) 61 | with DB: 62 | DB.execute("""INSERT INTO events (ven_id, event_id, request_id, status, event) 63 | VALUES (?, ?, ?, ?, ?)""", (ven_id, event_id, None, 'new', serialized_event)) 64 | 65 | async def _on_poll(ven_id, request_id=None): 66 | cur = DB.cursor() 67 | cur.execute("""SELECT event_id, event FROM events WHERE ven_id = ? AND status = 'new' LIMIT 1""", (ven_id,)) 68 | result = cur.fetchone() 69 | if result: 70 | event_id, event = result 71 | event_request_id = generate_id() 72 | with DB: 73 | DB.execute("""UPDATE events SET request_id = ? WHERE event_id = ?""", (event_request_id, event_id)) 74 | response_type = 'oadrDistributeEvent' 75 | response_payload = {'response': {'request_id': request_id, 76 | 'response_code': 200, 77 | 'response_description': 'OK'}, 78 | 'request_id': event_request_id, 79 | 'vtn_id': VTN_ID, 80 | 'events': [json.loads(event)]} 81 | else: 82 | response_type = 'oadrResponse' 83 | response_payload = {'response': {'request_id': request_id, 84 | 'response_code': 200, 85 | 'response_description': 'OK'}, 86 | 'ven_id': ven_id} 87 | return response_type, response_payload 88 | 89 | async def _on_create_party_registration(payload): 90 | registration_id = generate_id() 91 | ven_id = generate_id() 92 | add_ven(payload['ven_name'], ven_id, registration_id) 93 | payload = {'response': {'response_code': 200, 94 | 'response_description': 'OK', 95 | 'request_id': payload['request_id']}, 96 | 'ven_id': ven_id, 97 | 'registration_id': registration_id, 98 | 'profiles': [{'profile_name': '2.0b', 99 | 'transports': {'transport_name': 'simpleHttp'}}], 100 | 'requested_oadr_poll_freq': timedelta(seconds=10)} 101 | return 'oadrCreatedPartyRegistration', payload 102 | 103 | 104 | server = OpenADRServer(vtn_id=VTN_ID, http_port=SERVER_PORT) 105 | server.add_handler('on_create_party_registration', _on_create_party_registration) 106 | server.add_handler('on_poll', _on_poll) 107 | 108 | @pytest_asyncio.fixture 109 | async def start_server(): 110 | await server.run_async() 111 | yield 112 | await server.stop() 113 | -------------------------------------------------------------------------------- /test/integration_tests/test_client_registration.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from openleadr import OpenADRClient, OpenADRServer, enums 18 | from openleadr.utils import generate_id, certificate_fingerprint 19 | from openleadr.messaging import create_message, parse_message 20 | from datetime import datetime, timezone, timedelta 21 | 22 | import asyncio 23 | import sqlite3 24 | import pytest 25 | import pytest_asyncio 26 | from aiohttp import web 27 | 28 | import os 29 | 30 | SERVER_PORT = 8001 31 | VEN_NAME = 'myven' 32 | VEN_ID = '1234abcd' 33 | VTN_ID = "TestVTN" 34 | 35 | CERTFILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'certificates', 'dummy_ven.crt') 36 | KEYFILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'certificates', 'dummy_ven.key') 37 | CAFILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'certificates', 'dummy_ca.crt') 38 | 39 | async def _on_create_party_registration(payload): 40 | registration_id = generate_id() 41 | return VEN_ID, registration_id 42 | 43 | @pytest_asyncio.fixture 44 | async def start_server(): 45 | server = OpenADRServer(vtn_id=VTN_ID, http_port=SERVER_PORT) 46 | server.add_handler('on_create_party_registration', _on_create_party_registration) 47 | await server.run_async() 48 | yield 49 | await server.stop() 50 | 51 | @pytest_asyncio.fixture 52 | async def start_server_with_signatures(): 53 | server = OpenADRServer(vtn_id=VTN_ID, cert=CERTFILE, key=KEYFILE, fingerprint_lookup=fingerprint_lookup, http_port=SERVER_PORT) 54 | server.add_handler('on_create_party_registration', _on_create_party_registration) 55 | await server.run_async() 56 | yield 57 | await server.stop() 58 | 59 | 60 | @pytest.mark.asyncio 61 | async def test_query_party_registration(start_server): 62 | client = OpenADRClient(ven_name=VEN_NAME, 63 | vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b") 64 | 65 | response_type, response_payload = await client.query_registration() 66 | assert response_type == 'oadrCreatedPartyRegistration' 67 | assert response_payload['vtn_id'] == VTN_ID 68 | await client.stop() 69 | 70 | @pytest.mark.asyncio 71 | async def test_create_party_registration(start_server): 72 | client = OpenADRClient(ven_name=VEN_NAME, 73 | vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b") 74 | 75 | response_type, response_payload = await client.create_party_registration() 76 | assert response_type == 'oadrCreatedPartyRegistration' 77 | assert response_payload['ven_id'] == VEN_ID 78 | await client.stop() 79 | 80 | def fingerprint_lookup(ven_id): 81 | with open(CERTFILE) as file: 82 | cert = file.read() 83 | return certificate_fingerprint(cert) 84 | 85 | @pytest.mark.asyncio 86 | @pytest.mark.parametrize("disable_signature", [False, True]) 87 | async def test_create_party_registration_with_signatures(start_server_with_signatures, disable_signature): 88 | with open(CERTFILE) as file: 89 | cert = file.read() 90 | client = OpenADRClient(ven_name=VEN_NAME, 91 | vtn_url=f"http://localhost:{SERVER_PORT}/OpenADR2/Simple/2.0b", 92 | cert=CERTFILE, key=KEYFILE, ca_file=CAFILE, vtn_fingerprint=certificate_fingerprint(cert), 93 | disable_signature=disable_signature) 94 | 95 | response_type, response_payload = await client.create_party_registration() 96 | assert response_type == 'oadrCreatedPartyRegistration' 97 | assert response_payload['ven_id'] == VEN_ID 98 | await client.stop() 99 | -------------------------------------------------------------------------------- /test/test_certificates.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | import os 4 | from functools import partial 5 | from openleadr import OpenADRServer, OpenADRClient, enable_default_logging 6 | from openleadr.utils import certificate_fingerprint 7 | from openleadr import errors 8 | from async_timeout import timeout 9 | 10 | enable_default_logging() 11 | 12 | CA_CERT = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certificates', 'dummy_ca.crt') 13 | VTN_CERT = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certificates', 'dummy_vtn.crt') 14 | VTN_KEY = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certificates', 'dummy_vtn.key') 15 | VEN_CERT = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certificates', 'dummy_ven.crt') 16 | VEN_KEY = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'certificates', 'dummy_ven.key') 17 | 18 | 19 | with open(VEN_CERT) as file: 20 | ven_fingerprint = certificate_fingerprint(file.read()) 21 | 22 | with open(VTN_CERT) as file: 23 | vtn_fingerprint = certificate_fingerprint(file.read()) 24 | 25 | async def lookup_fingerprint(ven_id): 26 | return ven_fingerprint 27 | 28 | async def on_create_party_registration(payload, future): 29 | if payload['fingerprint'] != ven_fingerprint: 30 | raise errors.FingerprintMismatch("The fingerprint of your TLS connection does not match the expected fingerprint. Your VEN is not allowed to register.") 31 | else: 32 | future.set_result(True) 33 | return 'ven1234', 'reg5678' 34 | 35 | @pytest.mark.asyncio 36 | @pytest.mark.parametrize("disable_signature", [False, True]) 37 | async def test_ssl_certificates(disable_signature): 38 | loop = asyncio.get_event_loop() 39 | registration_future = loop.create_future() 40 | server = OpenADRServer(vtn_id='myvtn', 41 | http_cert=VTN_CERT, 42 | http_key=VTN_KEY, 43 | http_ca_file=CA_CERT, 44 | cert=VTN_CERT, 45 | key=VTN_KEY, 46 | fingerprint_lookup=lookup_fingerprint) 47 | server.add_handler('on_create_party_registration', partial(on_create_party_registration, 48 | future=registration_future)) 49 | await server.run_async() 50 | #await asyncio.sleep(1) 51 | # Run the client 52 | client = OpenADRClient(ven_name='myven', 53 | vtn_url='https://localhost:8080/OpenADR2/Simple/2.0b', 54 | cert=VEN_CERT, 55 | key=VEN_KEY, 56 | ca_file=CA_CERT, 57 | vtn_fingerprint=vtn_fingerprint, disable_signature=disable_signature) 58 | await client.run() 59 | 60 | # Wait for the registration to be triggered 61 | result = await asyncio.wait_for(registration_future, 1.0) 62 | assert client.registration_id == 'reg5678' 63 | 64 | await client.stop() 65 | await server.stop() 66 | #await asyncio.sleep(0) 67 | 68 | @pytest.mark.asyncio 69 | async def test_ssl_certificates_wrong_cert(): 70 | loop = asyncio.get_event_loop() 71 | registration_future = loop.create_future() 72 | server = OpenADRServer(vtn_id='myvtn', 73 | http_cert=VTN_CERT, 74 | http_key=VTN_KEY, 75 | http_ca_file=CA_CERT, 76 | cert=VTN_CERT, 77 | key=VTN_KEY, 78 | fingerprint_lookup=lookup_fingerprint) 79 | server.add_handler('on_create_party_registration', partial(on_create_party_registration, 80 | future=registration_future)) 81 | await server.run_async() 82 | #await asyncio.sleep(1) 83 | 84 | # Run the client 85 | client = OpenADRClient(ven_name='myven', 86 | vtn_url='https://localhost:8080/OpenADR2/Simple/2.0b', 87 | cert=VTN_CERT, 88 | key=VTN_KEY, 89 | ca_file=CA_CERT, 90 | vtn_fingerprint=vtn_fingerprint) 91 | await client.run() 92 | 93 | # Wait for the registration to be triggered 94 | with pytest.raises(asyncio.TimeoutError): 95 | await asyncio.wait_for(registration_future, timeout=0.5) 96 | assert client.registration_id is None 97 | 98 | await client.stop() 99 | await server.stop() 100 | await asyncio.sleep(0) 101 | 102 | @pytest.mark.asyncio 103 | async def test_ssl_certificates_wrong_fingerprint(caplog): 104 | loop = asyncio.get_event_loop() 105 | registration_future = loop.create_future() 106 | server = OpenADRServer(vtn_id='myvtn', 107 | http_cert=VTN_CERT, 108 | http_key=VTN_KEY, 109 | http_ca_file=CA_CERT, 110 | cert=VTN_CERT, 111 | key=VTN_KEY, 112 | fingerprint_lookup=lookup_fingerprint) 113 | server.add_handler('on_create_party_registration', partial(on_create_party_registration, 114 | future=registration_future)) 115 | await server.run_async() 116 | #await asyncio.sleep(1) 117 | # Run the client 118 | client = OpenADRClient(ven_name='myven', 119 | vtn_url='https://localhost:8080/OpenADR2/Simple/2.0b', 120 | cert=VEN_CERT, 121 | key=VEN_KEY, 122 | ca_file=CA_CERT, 123 | vtn_fingerprint='00:11:22:33:44:55:66:77:88:99') 124 | await client.run() 125 | 126 | # Wait for the registration to be triggered 127 | result = await asyncio.wait_for(registration_future, 1.0) 128 | 129 | assert client.registration_id is None 130 | assert ("The certificate fingerprint was incorrect. Expected: 00:11:22:33:44:55:66:77:88:99; " 131 | "Received: E6:0C:FE:2F:56:53:64:EA:EC:35. Ignoring message.") in [rec.message for rec in caplog.records] 132 | 133 | await client.stop() 134 | await server.stop() 135 | -------------------------------------------------------------------------------- /test/test_client_misc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from openleadr import OpenADRClient 3 | from openleadr import enums 4 | 5 | def test_trailing_slash_on_vtn_url(): 6 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost/') 7 | assert client.vtn_url == 'http://localhost' 8 | 9 | def test_wrong_handler_supplied(caplog): 10 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 11 | client.add_handler('non_existant', print) 12 | assert ("'handler' must be either on_event or on_update_event") in [rec.message for rec in caplog.records] 13 | 14 | def test_invalid_report_name(caplog): 15 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 16 | with pytest.raises(ValueError): 17 | client.add_report(callback=print, 18 | resource_id='myresource', 19 | measurement='voltage', 20 | report_name='non_existant') 21 | # assert (f"non_existant is not a valid report_name. Valid options are " 22 | # f"{', '.join(enums.REPORT_NAME.values)}", 23 | # " or any name starting with 'x-'.") in [rec.message for rec in caplog.records] 24 | 25 | def test_invalid_reading_type(): 26 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 27 | with pytest.raises(ValueError): 28 | client.add_report(callback=print, 29 | resource_id='myresource', 30 | measurement='voltage', 31 | reading_type='non_existant') 32 | # assert (f"non_existant is not a valid reading_type. Valid options are " 33 | # f"{', '.join(enums.READING_TYPE.values)}", 34 | # " or any name starting with 'x-'.") in [rec.message for rec in caplog.records] 35 | 36 | def test_invalid_report_type(): 37 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 38 | with pytest.raises(ValueError): 39 | client.add_report(callback=print, 40 | resource_id='myresource', 41 | measurement='voltage', 42 | report_type='non_existant') 43 | # assert (f"non_existant is not a valid report_type. Valid options are " 44 | # f"{', '.join(enums.REPORT_TYPE.values)}", 45 | # " or any name starting with 'x-'.") in [rec.message for rec in caplog.records] 46 | 47 | def test_invalid_data_collection_mode(): 48 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 49 | with pytest.raises(ValueError): 50 | client.add_report(callback=print, 51 | resource_id='myresource', 52 | measurement='voltage', 53 | data_collection_mode='non_existant') 54 | # assert ("The data_collection_mode should be 'incremental' or 'full'.") in [rec.message for rec in caplog.records] 55 | 56 | def test_invalid_scale(): 57 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 58 | with pytest.raises(ValueError): 59 | client.add_report(callback=print, 60 | resource_id='myresource', 61 | measurement='voltage', 62 | scale='non_existant') 63 | 64 | def test_add_report_without_specifier_id(): 65 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 66 | client.add_report(callback=print, 67 | resource_id='myresource1', 68 | measurement='voltage') 69 | client.add_report(callback=print, 70 | resource_id='myresource2', 71 | measurement='voltage') 72 | assert len(client.reports) == 1 73 | 74 | async def wrong_sig(param1): 75 | pass 76 | 77 | def test_add_report_with_invalid_callback_signature(): 78 | client = OpenADRClient(ven_name='myven', vtn_url='http://localhost') 79 | with pytest.raises(TypeError): 80 | client.add_report(callback=wrong_sig, 81 | data_collection_mode='full', 82 | resource_id='myresource1', 83 | measurement='voltage') 84 | -------------------------------------------------------------------------------- /test/test_errors.py: -------------------------------------------------------------------------------- 1 | from openleadr import errors, enums 2 | 3 | def test_protocol_errors(): 4 | for error in dir(errors): 5 | if isinstance(getattr(errors, error), type): 6 | err = getattr(errors, error)() 7 | if isinstance(err, errors.ProtocolError) and not type(err) == errors.ProtocolError: 8 | err_description = err.response_description 9 | err_code = err.response_code 10 | err_enum = err_description.replace(" ", "_") 11 | assert enums.STATUS_CODES[err_enum] == err_code -------------------------------------------------------------------------------- /test/test_poll_responses.py: -------------------------------------------------------------------------------- 1 | from openleadr import OpenADRClient, OpenADRServer, objects, utils 2 | from functools import partial 3 | from dataclasses import asdict 4 | from datetime import datetime, timezone, timedelta 5 | import pytest 6 | 7 | def on_create_party_registration(registration_info): 8 | return 'ven123', 'reg123' 9 | 10 | def poll_responder(ven_id, message_type, message_payload): 11 | return message_type, message_payload 12 | 13 | 14 | event = objects.Event(event_descriptor=objects.EventDescriptor(event_id='event123', 15 | event_status='far', 16 | modification_number='1', 17 | market_context='http://marketcontext01'), 18 | event_signals=[objects.EventSignal(signal_name='simple', 19 | signal_type='level', 20 | signal_id=utils.generate_id(), 21 | intervals=[objects.Interval(dtstart=datetime.now(timezone.utc), 22 | duration=timedelta(minutes=10), 23 | signal_payload=1)])], 24 | targets=[{'ven_id': 'ven123'}]) 25 | 26 | poll_responses = [('oadrResponse', {}), 27 | ('oadrDistributeEvent', {'events': [asdict(event)]}), 28 | ('oadrCreateReport', {'report_requests': [{'report_request_id': 'req123', 29 | 'report_specifier': {'report_specifier_id': 'rsi123', 30 | 'granularity': timedelta(seconds=10), 31 | 'report_back_duration': timedelta(seconds=10), 32 | 'specifier_payloads': [{'r_id': 'rid123', 33 | 'reading_type': 'Direct Read'}]}}]}), 34 | ('oadrCancelReport', {'report_request_id': 'report123', 35 | 'report_to_follow': False, 36 | 'request_id': 'request123'}), 37 | ('oadrRegisterReport', {'ven_id': 'ven123', 'reports': []}), 38 | ('oadrUpdateReport', {'ven_id': 'ven123'}), 39 | ('oadrCancelPartyRegistration', {'registration_id': 'reg123', 40 | 'ven_id': 'ven123'}), 41 | ('oadrRequestReregistration', {'ven_id': 'ven123'})] 42 | 43 | @pytest.mark.parametrize('message_type,message_payload', poll_responses) 44 | @pytest.mark.asyncio 45 | async def test_message(message_type, message_payload): 46 | server = OpenADRServer(vtn_id='myvtn') 47 | server.add_handler('on_create_party_registration', on_create_party_registration) 48 | server.add_handler('on_poll', partial(poll_responder, message_type=message_type, message_payload=message_payload)) 49 | client = OpenADRClient(ven_name='myven', 50 | vtn_url='http://localhost:8080/OpenADR2/Simple/2.0b') 51 | await server.run_async() 52 | await client.create_party_registration() 53 | response_type, response_payload = await client.poll() 54 | await server.stop() 55 | assert response_type == message_type 56 | -------------------------------------------------------------------------------- /test/test_registrations.py: -------------------------------------------------------------------------------- 1 | from openleadr import OpenADRClient, OpenADRServer, enable_default_logging 2 | import datetime 3 | import pytest 4 | import asyncio 5 | 6 | enable_default_logging() 7 | 8 | 9 | def on_create_party_registration_success(registration_info): 10 | return 'ven123', 'reg123' 11 | 12 | def on_create_party_registration_reject(registration_info): 13 | return False 14 | 15 | def on_event(*args, **kwargs): 16 | return 'optIn' 17 | 18 | def event_callback(ven_id, event_id, opt_type): 19 | pass 20 | 21 | @pytest.mark.asyncio 22 | async def test_successful_registration(): 23 | loop = asyncio.get_event_loop() 24 | client = OpenADRClient(ven_name='myven', 25 | vtn_url='http://localhost:8080/OpenADR2/Simple/2.0b') 26 | 27 | client.add_handler('on_event', on_event) 28 | server = OpenADRServer(vtn_id='myvtn', requested_poll_freq=datetime.timedelta(seconds=1)) 29 | server.add_handler('on_create_party_registration', on_create_party_registration_success) 30 | 31 | await server.run_async() 32 | await client.run() 33 | 34 | await asyncio.sleep(0.1) 35 | 36 | assert client.registration_id == 'reg123' 37 | await client.stop() 38 | await server.stop() 39 | 40 | @pytest.mark.asyncio 41 | async def test_rejected_registration(): 42 | loop = asyncio.get_event_loop() 43 | client = OpenADRClient(ven_name='myven', 44 | vtn_url='http://localhost:8080/OpenADR2/Simple/2.0b') 45 | 46 | client.add_handler('on_event', on_event) 47 | server = OpenADRServer(vtn_id='myvtn', requested_poll_freq=datetime.timedelta(seconds=1)) 48 | server.add_handler('on_create_party_registration', on_create_party_registration_reject) 49 | 50 | await server.run_async() 51 | await client.run() 52 | 53 | await asyncio.sleep(0.1) 54 | 55 | assert client.registration_id == None 56 | await client.stop() 57 | await server.stop() 58 | 59 | @pytest.mark.asyncio 60 | async def test_registration_with_prefilled_ven_id(): 61 | loop = asyncio.get_event_loop() 62 | client = OpenADRClient(ven_name='myven', 63 | vtn_url='http://localhost:8080/OpenADR2/Simple/2.0b', 64 | ven_id='ven123') 65 | 66 | client.add_handler('on_event', on_event) 67 | server = OpenADRServer(vtn_id='myvtn', requested_poll_freq=datetime.timedelta(seconds=1)) 68 | server.add_handler('on_create_party_registration', on_create_party_registration_success) 69 | 70 | await server.run_async() 71 | await client.run() 72 | 73 | await asyncio.sleep(0.1) 74 | 75 | assert client.registration_id == 'reg123' 76 | await client.stop() 77 | await server.stop() 78 | 79 | @pytest.mark.asyncio 80 | async def test_rejected_registration_with_prefilled_ven_id(): 81 | loop = asyncio.get_event_loop() 82 | client = OpenADRClient(ven_name='myven', 83 | vtn_url='http://localhost:8080/OpenADR2/Simple/2.0b', 84 | ven_id='ven123') 85 | 86 | client.add_handler('on_event', on_event) 87 | server = OpenADRServer(vtn_id='myvtn', requested_poll_freq=datetime.timedelta(seconds=1)) 88 | server.add_handler('on_create_party_registration', on_create_party_registration_reject) 89 | 90 | await server.run_async() 91 | await client.run() 92 | 93 | await asyncio.sleep(0.1) 94 | 95 | assert client.registration_id == None 96 | await client.stop() 97 | await server.stop() 98 | 99 | @pytest.mark.asyncio 100 | async def test_registration_with_different_ven_id(): 101 | loop = asyncio.get_event_loop() 102 | client = OpenADRClient(ven_name='myven', 103 | vtn_url='http://localhost:8080/OpenADR2/Simple/2.0b', 104 | ven_id='someven') 105 | client.add_handler('on_event', on_event) 106 | server = OpenADRServer(vtn_id='myvtn', requested_poll_freq=datetime.timedelta(seconds=1)) 107 | server.add_handler('on_create_party_registration', on_create_party_registration_success) 108 | 109 | await server.run_async() 110 | await client.run() 111 | 112 | await asyncio.sleep(0.1) 113 | 114 | assert client.registration_id == 'reg123' 115 | assert client.ven_id == 'ven123' 116 | 117 | await asyncio.sleep(0.1) 118 | 119 | await client.stop() 120 | await server.stop() 121 | -------------------------------------------------------------------------------- /test/test_signatures.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # Copyright 2020 Contributors to OpenLEADR 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | from openleadr.utils import generate_id, certificate_fingerprint, ensure_bytes 19 | from openleadr.messaging import create_message, parse_message, validate_xml_signature, validate_xml_schema, validate_xml_signature_none 20 | from hashlib import sha256 21 | from base64 import b64encode 22 | from datetime import datetime, timedelta, timezone 23 | from lxml import etree 24 | import os 25 | 26 | with open(os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'certificates', 'dummy_ven.crt'), 'rb') as file: 27 | TEST_CERT = file.read() 28 | with open(os.path.join(os.path.abspath(os.path.dirname(os.path.dirname(__file__))), 'certificates', 'dummy_ven.key'), 'rb') as file: 29 | TEST_KEY = file.read() 30 | TEST_KEY_PASSWORD = 'openadr' 31 | 32 | def test_message_validation(): 33 | msg = create_message('oadrPoll', ven_id='123', cert=TEST_CERT, key=TEST_KEY) 34 | tree = etree.fromstring(msg.encode('utf-8')) 35 | validate_xml_signature(tree) 36 | parsed_type, parsed_message = parse_message(msg) 37 | assert parsed_type == 'oadrPoll' 38 | 39 | def test_message_validation_disable_signature(): 40 | msg = create_message('oadrPoll', ven_id='123', cert=TEST_CERT, key=TEST_KEY, disable_signature=True) 41 | tree = etree.fromstring(msg.encode('utf-8')) 42 | validate_xml_signature_none(tree) 43 | parsed_type, parsed_message = parse_message(msg) 44 | assert parsed_type == 'oadrPoll' 45 | 46 | 47 | def test_message_validation_complex(): 48 | now = datetime.now(timezone.utc) 49 | event_id = generate_id() 50 | active_period = {"dtstart": now + timedelta(minutes=1), 51 | "duration": timedelta(minutes=9)} 52 | 53 | event_descriptor = {"event_id": event_id, 54 | "modification_number": 1, 55 | "modification_date_time": now, 56 | "priority": 1, 57 | "market_context": "http://MarketContext1", 58 | "created_date_time": now, 59 | "event_status": "near", 60 | "test_event": "false", 61 | "vtn_comment": "This is an event"} 62 | 63 | event_signals = [{"intervals": [{"duration": timedelta(minutes=1), "uid": 1, "signal_payload": 8}, 64 | {"duration": timedelta(minutes=1), "uid": 2, "signal_payload": 10}, 65 | {"duration": timedelta(minutes=1), "uid": 3, "signal_payload": 12}, 66 | {"duration": timedelta(minutes=1), "uid": 4, "signal_payload": 14}, 67 | {"duration": timedelta(minutes=1), "uid": 5, "signal_payload": 16}, 68 | {"duration": timedelta(minutes=1), "uid": 6, "signal_payload": 18}, 69 | {"duration": timedelta(minutes=1), "uid": 7, "signal_payload": 20}, 70 | {"duration": timedelta(minutes=1), "uid": 8, "signal_payload": 10}, 71 | {"duration": timedelta(minutes=1), "uid": 9, "signal_payload": 20}], 72 | "signal_name": "LOAD_CONTROL", 73 | #"signal_name": "simple", 74 | #"signal_type": "level", 75 | "signal_type": "x-loadControlCapacity", 76 | "signal_id": generate_id(), 77 | "current_value": 9.99}] 78 | 79 | event_targets = [{"ven_id": 'VEN001'}, {"ven_id": 'VEN002'}] 80 | event = {'active_period': active_period, 81 | 'event_descriptor': event_descriptor, 82 | 'event_signals': event_signals, 83 | 'targets': event_targets, 84 | 'response_required': 'always'} 85 | 86 | msg = create_message('oadrDistributeEvent', 87 | request_id=generate_id(), 88 | response={'request_id': 123, 'response_code': 200, 'response_description': 'OK'}, 89 | events=[event], 90 | cert=TEST_CERT, 91 | key=TEST_KEY) 92 | tree = etree.fromstring(msg.encode('utf-8')) 93 | validate_xml_signature(tree) 94 | parsed_type, parsed_msg = parse_message(msg) 95 | 96 | --------------------------------------------------------------------------------